@assistant-ui/react-streamdown 0.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/README.md +163 -0
- package/dist/adapters/PreOverride.d.ts +27 -0
- package/dist/adapters/PreOverride.d.ts.map +1 -0
- package/dist/adapters/PreOverride.js +31 -0
- package/dist/adapters/PreOverride.js.map +1 -0
- package/dist/adapters/code-adapter.d.ts +22 -0
- package/dist/adapters/code-adapter.d.ts.map +1 -0
- package/dist/adapters/code-adapter.js +75 -0
- package/dist/adapters/code-adapter.js.map +1 -0
- package/dist/adapters/components-adapter.d.ts +18 -0
- package/dist/adapters/components-adapter.d.ts.map +1 -0
- package/dist/adapters/components-adapter.js +34 -0
- package/dist/adapters/components-adapter.js.map +1 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +37 -0
- package/dist/defaults.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/memoization.d.ts +10 -0
- package/dist/memoization.d.ts.map +1 -0
- package/dist/memoization.js +30 -0
- package/dist/memoization.js.map +1 -0
- package/dist/primitives/StreamdownText.d.ts +60 -0
- package/dist/primitives/StreamdownText.d.ts.map +1 -0
- package/dist/primitives/StreamdownText.js +124 -0
- package/dist/primitives/StreamdownText.js.map +1 -0
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
- package/src/__tests__/PreOverride.test.tsx +132 -0
- package/src/__tests__/code-adapter.integration.test.tsx +325 -0
- package/src/__tests__/code-adapter.test.tsx +46 -0
- package/src/__tests__/components-adapter.test.tsx +152 -0
- package/src/__tests__/defaults.test.ts +96 -0
- package/src/__tests__/index.test.ts +40 -0
- package/src/__tests__/memoization.test.ts +71 -0
- package/src/adapters/PreOverride.tsx +52 -0
- package/src/adapters/code-adapter.tsx +148 -0
- package/src/adapters/components-adapter.tsx +51 -0
- package/src/defaults.ts +46 -0
- package/src/index.ts +45 -0
- package/src/memoization.ts +38 -0
- package/src/primitives/StreamdownText.tsx +201 -0
- package/src/types.ts +416 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { render, renderHook, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
PreContext,
|
|
6
|
+
PreOverride,
|
|
7
|
+
useIsStreamdownCodeBlock,
|
|
8
|
+
useStreamdownPreProps,
|
|
9
|
+
} from "../adapters/PreOverride";
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup);
|
|
12
|
+
|
|
13
|
+
describe("useIsStreamdownCodeBlock", () => {
|
|
14
|
+
it("returns false when not inside PreContext", () => {
|
|
15
|
+
const { result } = renderHook(() => useIsStreamdownCodeBlock());
|
|
16
|
+
expect(result.current).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns true when inside PreContext", () => {
|
|
20
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
21
|
+
<PreContext.Provider value={{ className: "test" }}>
|
|
22
|
+
{children}
|
|
23
|
+
</PreContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const { result } = renderHook(() => useIsStreamdownCodeBlock(), {
|
|
27
|
+
wrapper,
|
|
28
|
+
});
|
|
29
|
+
expect(result.current).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns true with minimal context value", () => {
|
|
33
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
34
|
+
<PreContext.Provider value={{}}>{children}</PreContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useIsStreamdownCodeBlock(), {
|
|
38
|
+
wrapper,
|
|
39
|
+
});
|
|
40
|
+
expect(result.current).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("useStreamdownPreProps", () => {
|
|
45
|
+
it("returns null when not inside PreContext", () => {
|
|
46
|
+
const { result } = renderHook(() => useStreamdownPreProps());
|
|
47
|
+
expect(result.current).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns context value when inside PreContext", () => {
|
|
51
|
+
const preProps = { className: "test-class", "data-foo": "bar" };
|
|
52
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
53
|
+
<PreContext.Provider value={preProps}>{children}</PreContext.Provider>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const { result } = renderHook(() => useStreamdownPreProps(), { wrapper });
|
|
57
|
+
expect(result.current).toEqual(preProps);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("includes node when provided", () => {
|
|
61
|
+
const mockNode = {
|
|
62
|
+
type: "element" as const,
|
|
63
|
+
tagName: "pre",
|
|
64
|
+
position: { start: { line: 1 }, end: { line: 5 } },
|
|
65
|
+
};
|
|
66
|
+
const preProps = { node: mockNode, className: "test" };
|
|
67
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
68
|
+
<PreContext.Provider value={preProps}>{children}</PreContext.Provider>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useStreamdownPreProps(), { wrapper });
|
|
72
|
+
expect(result.current?.node).toEqual(mockNode);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("PreOverride component", () => {
|
|
77
|
+
it("renders a pre element", () => {
|
|
78
|
+
render(<PreOverride>code content</PreOverride>);
|
|
79
|
+
const preElement = screen.getByText("code content");
|
|
80
|
+
expect(preElement.tagName).toBe("PRE");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("passes through props to pre element", () => {
|
|
84
|
+
render(
|
|
85
|
+
<PreOverride className="my-class" data-testid="my-pre">
|
|
86
|
+
content
|
|
87
|
+
</PreOverride>,
|
|
88
|
+
);
|
|
89
|
+
const preElement = screen.getByTestId("my-pre");
|
|
90
|
+
expect(preElement.className).toContain("my-class");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("provides context to children", () => {
|
|
94
|
+
function ChildComponent() {
|
|
95
|
+
const isCodeBlock = useIsStreamdownCodeBlock();
|
|
96
|
+
return <span data-testid="result">{isCodeBlock ? "yes" : "no"}</span>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
render(
|
|
100
|
+
<PreOverride>
|
|
101
|
+
<ChildComponent />
|
|
102
|
+
</PreOverride>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(screen.getByTestId("result").textContent).toBe("yes");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("provides props via context", () => {
|
|
109
|
+
function ChildComponent() {
|
|
110
|
+
const props = useStreamdownPreProps();
|
|
111
|
+
return <span data-testid="result">{props?.className}</span>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(
|
|
115
|
+
<PreOverride className="test-class">
|
|
116
|
+
<ChildComponent />
|
|
117
|
+
</PreOverride>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(screen.getByTestId("result").textContent).toBe("test-class");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("is memoized and does not re-render unnecessarily", () => {
|
|
124
|
+
const { rerender } = render(
|
|
125
|
+
<PreOverride className="test">content</PreOverride>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Same props should not cause issues
|
|
129
|
+
rerender(<PreOverride className="test">content</PreOverride>);
|
|
130
|
+
expect(screen.getByText("content")).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { createCodeAdapter } from "../adapters/code-adapter";
|
|
5
|
+
import { PreContext } from "../adapters/PreOverride";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
// Wrapper to provide PreContext (simulates being inside a code block)
|
|
10
|
+
function CodeBlockWrapper({ children }: { children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<PreContext.Provider value={{ className: "language-javascript" }}>
|
|
13
|
+
{children}
|
|
14
|
+
</PreContext.Provider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("createCodeAdapter integration", () => {
|
|
19
|
+
describe("inline code detection", () => {
|
|
20
|
+
it("renders inline code when not inside PreContext", () => {
|
|
21
|
+
const AdaptedCode = createCodeAdapter({});
|
|
22
|
+
render(<AdaptedCode className="inline">console.log</AdaptedCode>);
|
|
23
|
+
|
|
24
|
+
const codeElement = screen.getByText("console.log");
|
|
25
|
+
expect(codeElement.tagName).toBe("CODE");
|
|
26
|
+
expect(codeElement.className).toContain("aui-streamdown-inline-code");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("applies inline class along with user class", () => {
|
|
30
|
+
const AdaptedCode = createCodeAdapter({});
|
|
31
|
+
render(<AdaptedCode className="custom-class">code</AdaptedCode>);
|
|
32
|
+
|
|
33
|
+
const codeElement = screen.getByText("code");
|
|
34
|
+
expect(codeElement.className).toContain("aui-streamdown-inline-code");
|
|
35
|
+
expect(codeElement.className).toContain("custom-class");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("code block detection", () => {
|
|
40
|
+
it("uses SyntaxHighlighter when inside PreContext", () => {
|
|
41
|
+
const MockSyntax = vi.fn(({ code, language }) => (
|
|
42
|
+
<div data-testid="syntax">{`${language}: ${code}`}</div>
|
|
43
|
+
));
|
|
44
|
+
const AdaptedCode = createCodeAdapter({
|
|
45
|
+
SyntaxHighlighter: MockSyntax,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
render(
|
|
49
|
+
<CodeBlockWrapper>
|
|
50
|
+
<AdaptedCode className="language-javascript">const x = 1</AdaptedCode>
|
|
51
|
+
</CodeBlockWrapper>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(screen.getByTestId("syntax").textContent).toBe(
|
|
55
|
+
"javascript: const x = 1",
|
|
56
|
+
);
|
|
57
|
+
expect(MockSyntax).toHaveBeenCalled();
|
|
58
|
+
const callArgs = MockSyntax.mock.calls[0][0];
|
|
59
|
+
expect(callArgs.language).toBe("javascript");
|
|
60
|
+
expect(callArgs.code).toBe("const x = 1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders CodeHeader when provided", () => {
|
|
64
|
+
const MockHeader = vi.fn(({ language }) => (
|
|
65
|
+
<div data-testid="header">{language}</div>
|
|
66
|
+
));
|
|
67
|
+
const MockSyntax = vi.fn(({ code }) => (
|
|
68
|
+
<div data-testid="syntax">{code}</div>
|
|
69
|
+
));
|
|
70
|
+
|
|
71
|
+
const AdaptedCode = createCodeAdapter({
|
|
72
|
+
CodeHeader: MockHeader,
|
|
73
|
+
SyntaxHighlighter: MockSyntax,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<CodeBlockWrapper>
|
|
78
|
+
<AdaptedCode className="language-python">print("hi")</AdaptedCode>
|
|
79
|
+
</CodeBlockWrapper>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByTestId("header").textContent).toBe("python");
|
|
83
|
+
expect(screen.getByTestId("syntax").textContent).toBe('print("hi")');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("language detection", () => {
|
|
88
|
+
it.each([
|
|
89
|
+
["language-javascript", "javascript"],
|
|
90
|
+
["language-typescript", "typescript"],
|
|
91
|
+
["language-python", "python"],
|
|
92
|
+
["language-rust", "rust"],
|
|
93
|
+
["language-go", "go"],
|
|
94
|
+
["language-c++", "c++"],
|
|
95
|
+
["language-c#", "c#"],
|
|
96
|
+
["language-", ""],
|
|
97
|
+
["", ""],
|
|
98
|
+
[undefined, ""],
|
|
99
|
+
])("extracts %s as %s", (className, expected) => {
|
|
100
|
+
const MockSyntax = vi.fn(({ language }) => (
|
|
101
|
+
<div data-testid="syntax">{language}</div>
|
|
102
|
+
));
|
|
103
|
+
const AdaptedCode = createCodeAdapter({ SyntaxHighlighter: MockSyntax });
|
|
104
|
+
|
|
105
|
+
render(
|
|
106
|
+
<CodeBlockWrapper>
|
|
107
|
+
<AdaptedCode className={className}>code</AdaptedCode>
|
|
108
|
+
</CodeBlockWrapper>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByTestId("syntax").textContent).toBe(expected);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("componentsByLanguage", () => {
|
|
116
|
+
it("uses language-specific SyntaxHighlighter for matching language", () => {
|
|
117
|
+
const PythonSyntax = vi.fn(() => (
|
|
118
|
+
<div data-testid="python">python specific</div>
|
|
119
|
+
));
|
|
120
|
+
const AdaptedCode = createCodeAdapter({
|
|
121
|
+
SyntaxHighlighter: () => <div>default</div>,
|
|
122
|
+
componentsByLanguage: { python: { SyntaxHighlighter: PythonSyntax } },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
render(
|
|
126
|
+
<PreContext.Provider value={{}}>
|
|
127
|
+
<AdaptedCode className="language-python">code</AdaptedCode>
|
|
128
|
+
</PreContext.Provider>,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(screen.getByTestId("python")).toBeDefined();
|
|
132
|
+
expect(PythonSyntax).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("uses default SyntaxHighlighter for non-matching language", () => {
|
|
136
|
+
const DefaultSyntax = vi.fn(() => (
|
|
137
|
+
<div data-testid="default">default</div>
|
|
138
|
+
));
|
|
139
|
+
const AdaptedCode = createCodeAdapter({
|
|
140
|
+
SyntaxHighlighter: DefaultSyntax,
|
|
141
|
+
componentsByLanguage: { python: { SyntaxHighlighter: () => <div /> } },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
render(
|
|
145
|
+
<PreContext.Provider value={{}}>
|
|
146
|
+
<AdaptedCode className="language-javascript">code</AdaptedCode>
|
|
147
|
+
</PreContext.Provider>,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(screen.getByTestId("default")).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("uses language-specific CodeHeader", () => {
|
|
154
|
+
const DefaultHeader = vi.fn(() => (
|
|
155
|
+
<div data-testid="default-header">default</div>
|
|
156
|
+
));
|
|
157
|
+
const MermaidHeader = vi.fn(() => (
|
|
158
|
+
<div data-testid="mermaid-header">mermaid</div>
|
|
159
|
+
));
|
|
160
|
+
const MockSyntax = vi.fn(() => <div>syntax</div>);
|
|
161
|
+
|
|
162
|
+
const AdaptedCode = createCodeAdapter({
|
|
163
|
+
SyntaxHighlighter: MockSyntax,
|
|
164
|
+
CodeHeader: DefaultHeader,
|
|
165
|
+
componentsByLanguage: {
|
|
166
|
+
mermaid: { CodeHeader: MermaidHeader },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
<PreContext.Provider value={{}}>
|
|
172
|
+
<AdaptedCode className="language-mermaid">code</AdaptedCode>
|
|
173
|
+
</PreContext.Provider>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(screen.getByTestId("mermaid-header")).toBeDefined();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("code extraction", () => {
|
|
181
|
+
it("extracts string children", () => {
|
|
182
|
+
const MockSyntax = vi.fn(({ code }) => (
|
|
183
|
+
<div data-testid="syntax">{code}</div>
|
|
184
|
+
));
|
|
185
|
+
const AdaptedCode = createCodeAdapter({
|
|
186
|
+
SyntaxHighlighter: MockSyntax,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
render(
|
|
190
|
+
<CodeBlockWrapper>
|
|
191
|
+
<AdaptedCode className="language-js">const x = 1;</AdaptedCode>
|
|
192
|
+
</CodeBlockWrapper>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(screen.getByTestId("syntax").textContent).toBe("const x = 1;");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("extracts code from React element children", () => {
|
|
199
|
+
const MockSyntax = vi.fn(({ code }) => (
|
|
200
|
+
<div data-testid="syntax">{code}</div>
|
|
201
|
+
));
|
|
202
|
+
const AdaptedCode = createCodeAdapter({
|
|
203
|
+
SyntaxHighlighter: MockSyntax,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Simulates how streamdown might pass children
|
|
207
|
+
const nestedElement = <span>nested code</span>;
|
|
208
|
+
|
|
209
|
+
render(
|
|
210
|
+
<CodeBlockWrapper>
|
|
211
|
+
<AdaptedCode className="language-js">{nestedElement}</AdaptedCode>
|
|
212
|
+
</CodeBlockWrapper>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(MockSyntax).toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("handles empty children", () => {
|
|
219
|
+
const MockSyntax = vi.fn(({ code }) => (
|
|
220
|
+
<div data-testid="syntax-empty">{code || "empty"}</div>
|
|
221
|
+
));
|
|
222
|
+
const AdaptedCode = createCodeAdapter({
|
|
223
|
+
SyntaxHighlighter: MockSyntax,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
render(
|
|
227
|
+
<CodeBlockWrapper>
|
|
228
|
+
<AdaptedCode className="language-js">{""}</AdaptedCode>
|
|
229
|
+
</CodeBlockWrapper>,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(screen.getByTestId("syntax-empty").textContent).toBe("empty");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("null return for streamdown default", () => {
|
|
237
|
+
it("returns null when no custom SyntaxHighlighter", () => {
|
|
238
|
+
const AdaptedCode = createCodeAdapter({});
|
|
239
|
+
|
|
240
|
+
const { container } = render(
|
|
241
|
+
<CodeBlockWrapper>
|
|
242
|
+
<AdaptedCode className="language-js">code</AdaptedCode>
|
|
243
|
+
</CodeBlockWrapper>,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// When no SyntaxHighlighter provided for block code, returns null
|
|
247
|
+
// React renders nothing for null
|
|
248
|
+
expect(container.querySelector("code")).toBeNull();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("Pre and Code component props", () => {
|
|
253
|
+
it("passes Pre and Code components to SyntaxHighlighter", () => {
|
|
254
|
+
const MockSyntax = vi.fn(({ components }) => {
|
|
255
|
+
const { Pre, Code } = components;
|
|
256
|
+
return (
|
|
257
|
+
<Pre>
|
|
258
|
+
<Code data-testid="inner-code">test</Code>
|
|
259
|
+
</Pre>
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const AdaptedCode = createCodeAdapter({
|
|
264
|
+
SyntaxHighlighter: MockSyntax,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
render(
|
|
268
|
+
<CodeBlockWrapper>
|
|
269
|
+
<AdaptedCode className="language-js">test</AdaptedCode>
|
|
270
|
+
</CodeBlockWrapper>,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(screen.getByTestId("inner-code")).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("default Pre strips node prop", () => {
|
|
277
|
+
const MockSyntax = vi.fn(({ components }) => {
|
|
278
|
+
const { Pre } = components;
|
|
279
|
+
// Pre should render a pre element and strip node
|
|
280
|
+
return (
|
|
281
|
+
<Pre node={undefined} data-testid="pre">
|
|
282
|
+
content
|
|
283
|
+
</Pre>
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const AdaptedCode = createCodeAdapter({
|
|
288
|
+
SyntaxHighlighter: MockSyntax,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
render(
|
|
292
|
+
<CodeBlockWrapper>
|
|
293
|
+
<AdaptedCode className="language-js">test</AdaptedCode>
|
|
294
|
+
</CodeBlockWrapper>,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const preElement = screen.getByTestId("pre");
|
|
298
|
+
expect(preElement.tagName).toBe("PRE");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("default Code strips node prop", () => {
|
|
302
|
+
const MockSyntax = vi.fn(({ components }) => {
|
|
303
|
+
const { Code } = components;
|
|
304
|
+
return (
|
|
305
|
+
<Code node={undefined} data-testid="code">
|
|
306
|
+
content
|
|
307
|
+
</Code>
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const AdaptedCode = createCodeAdapter({
|
|
312
|
+
SyntaxHighlighter: MockSyntax,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
render(
|
|
316
|
+
<CodeBlockWrapper>
|
|
317
|
+
<AdaptedCode className="language-js">test</AdaptedCode>
|
|
318
|
+
</CodeBlockWrapper>,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const codeElement = screen.getByTestId("code");
|
|
322
|
+
expect(codeElement.tagName).toBe("CODE");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { shouldUseCodeAdapter } from "../adapters/code-adapter";
|
|
3
|
+
|
|
4
|
+
describe("shouldUseCodeAdapter", () => {
|
|
5
|
+
it("returns false when no custom components provided", () => {
|
|
6
|
+
expect(shouldUseCodeAdapter({})).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns true when SyntaxHighlighter is provided", () => {
|
|
10
|
+
const MockSyntaxHighlighter = () => null;
|
|
11
|
+
expect(
|
|
12
|
+
shouldUseCodeAdapter({ SyntaxHighlighter: MockSyntaxHighlighter }),
|
|
13
|
+
).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns true when CodeHeader is provided", () => {
|
|
17
|
+
const MockCodeHeader = () => null;
|
|
18
|
+
expect(shouldUseCodeAdapter({ CodeHeader: MockCodeHeader })).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns true when componentsByLanguage has entries", () => {
|
|
22
|
+
expect(
|
|
23
|
+
shouldUseCodeAdapter({
|
|
24
|
+
componentsByLanguage: {
|
|
25
|
+
mermaid: { SyntaxHighlighter: () => null },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns false when componentsByLanguage is empty", () => {
|
|
32
|
+
expect(shouldUseCodeAdapter({ componentsByLanguage: {} })).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns true when multiple options are provided", () => {
|
|
36
|
+
expect(
|
|
37
|
+
shouldUseCodeAdapter({
|
|
38
|
+
SyntaxHighlighter: () => null,
|
|
39
|
+
CodeHeader: () => null,
|
|
40
|
+
componentsByLanguage: {
|
|
41
|
+
python: { SyntaxHighlighter: () => null },
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { useAdaptedComponents } from "../adapters/components-adapter";
|
|
4
|
+
|
|
5
|
+
describe("useAdaptedComponents", () => {
|
|
6
|
+
describe("basic behavior", () => {
|
|
7
|
+
it("returns PreOverride when no components provided", () => {
|
|
8
|
+
const { result } = renderHook(() => useAdaptedComponents({}));
|
|
9
|
+
expect(result.current).toHaveProperty("pre");
|
|
10
|
+
expect(result.current).not.toHaveProperty("code");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("includes user HTML components", () => {
|
|
14
|
+
const MockDiv = vi.fn(() => null);
|
|
15
|
+
const { result } = renderHook(() =>
|
|
16
|
+
useAdaptedComponents({
|
|
17
|
+
components: { div: MockDiv },
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
expect(result.current?.div).toBe(MockDiv);
|
|
21
|
+
expect(result.current).toHaveProperty("pre");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("excludes SyntaxHighlighter and CodeHeader from direct pass-through", () => {
|
|
25
|
+
const MockSyntax = vi.fn(() => null);
|
|
26
|
+
const MockHeader = vi.fn(() => null);
|
|
27
|
+
const { result } = renderHook(() =>
|
|
28
|
+
useAdaptedComponents({
|
|
29
|
+
components: {
|
|
30
|
+
SyntaxHighlighter: MockSyntax,
|
|
31
|
+
CodeHeader: MockHeader,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
// These should be used to create code adapter, not passed directly
|
|
36
|
+
expect(result.current).toHaveProperty("code");
|
|
37
|
+
expect(result.current).toHaveProperty("pre");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("with SyntaxHighlighter", () => {
|
|
42
|
+
it("creates code adapter when SyntaxHighlighter provided", () => {
|
|
43
|
+
const MockSyntax = vi.fn(() => null);
|
|
44
|
+
const { result } = renderHook(() =>
|
|
45
|
+
useAdaptedComponents({
|
|
46
|
+
components: { SyntaxHighlighter: MockSyntax },
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
expect(result.current).toHaveProperty("code");
|
|
50
|
+
expect(typeof result.current?.code).toBe("function");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("creates code adapter when CodeHeader provided", () => {
|
|
54
|
+
const MockHeader = vi.fn(() => null);
|
|
55
|
+
const { result } = renderHook(() =>
|
|
56
|
+
useAdaptedComponents({
|
|
57
|
+
components: { CodeHeader: MockHeader },
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
expect(result.current).toHaveProperty("code");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("with componentsByLanguage", () => {
|
|
65
|
+
it("creates code adapter when componentsByLanguage provided", () => {
|
|
66
|
+
const MockMermaid = vi.fn(() => null);
|
|
67
|
+
const { result } = renderHook(() =>
|
|
68
|
+
useAdaptedComponents({
|
|
69
|
+
componentsByLanguage: {
|
|
70
|
+
mermaid: { SyntaxHighlighter: MockMermaid },
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
expect(result.current).toHaveProperty("code");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles multiple language overrides", () => {
|
|
78
|
+
const MockPython = vi.fn(() => null);
|
|
79
|
+
const MockRust = vi.fn(() => null);
|
|
80
|
+
const { result } = renderHook(() =>
|
|
81
|
+
useAdaptedComponents({
|
|
82
|
+
componentsByLanguage: {
|
|
83
|
+
python: { SyntaxHighlighter: MockPython },
|
|
84
|
+
rust: { SyntaxHighlighter: MockRust },
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
expect(result.current).toHaveProperty("code");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("handles empty componentsByLanguage", () => {
|
|
92
|
+
const { result } = renderHook(() =>
|
|
93
|
+
useAdaptedComponents({
|
|
94
|
+
componentsByLanguage: {},
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
// Empty componentsByLanguage should not create code adapter
|
|
98
|
+
expect(result.current).not.toHaveProperty("code");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("memoization", () => {
|
|
103
|
+
it("returns same reference when deps unchanged", () => {
|
|
104
|
+
const components = { div: vi.fn(() => null) };
|
|
105
|
+
const { result, rerender } = renderHook(
|
|
106
|
+
({ comps }) => useAdaptedComponents({ components: comps }),
|
|
107
|
+
{ initialProps: { comps: components } },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const firstResult = result.current;
|
|
111
|
+
rerender({ comps: components });
|
|
112
|
+
expect(result.current).toBe(firstResult);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns new reference when components change", () => {
|
|
116
|
+
const { result, rerender } = renderHook(
|
|
117
|
+
({ comps }) => useAdaptedComponents({ components: comps }),
|
|
118
|
+
{ initialProps: { comps: { div: vi.fn(() => null) } } },
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const firstResult = result.current;
|
|
122
|
+
rerender({ comps: { span: vi.fn(() => null) } });
|
|
123
|
+
expect(result.current).not.toBe(firstResult);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("combined options", () => {
|
|
128
|
+
it("handles all options together", () => {
|
|
129
|
+
const MockDiv = vi.fn(() => null);
|
|
130
|
+
const MockSyntax = vi.fn(() => null);
|
|
131
|
+
const MockHeader = vi.fn(() => null);
|
|
132
|
+
const MockMermaid = vi.fn(() => null);
|
|
133
|
+
|
|
134
|
+
const { result } = renderHook(() =>
|
|
135
|
+
useAdaptedComponents({
|
|
136
|
+
components: {
|
|
137
|
+
div: MockDiv,
|
|
138
|
+
SyntaxHighlighter: MockSyntax,
|
|
139
|
+
CodeHeader: MockHeader,
|
|
140
|
+
},
|
|
141
|
+
componentsByLanguage: {
|
|
142
|
+
mermaid: { SyntaxHighlighter: MockMermaid },
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(result.current?.div).toBe(MockDiv);
|
|
148
|
+
expect(result.current).toHaveProperty("code");
|
|
149
|
+
expect(result.current).toHaveProperty("pre");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|