feat: 增强 Markdown 代码块高亮和表格样式
This commit is contained in:
87
tests/web/components/chat/CodeBlock.test.tsx
Normal file
87
tests/web/components/chat/CodeBlock.test.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { createElement } from "react";
|
||||
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
void mock.module("../../../../src/web/shared/hooks/use-is-dark", () => ({
|
||||
useIsDark: () => false,
|
||||
}));
|
||||
|
||||
void mock.module("shiki", () => ({
|
||||
codeToHtml: (code: string, options: { lang: string; theme: string }) =>
|
||||
Promise.resolve(`<pre class="shiki ${options.theme}"><code>${code}</code></pre>`),
|
||||
}));
|
||||
|
||||
import { CodeBlock } from "../../../../src/web/features/chat/parts/CodeBlock";
|
||||
|
||||
function createCodeChild(text: string, lang = "javascript"): React.ReactElement {
|
||||
return createElement("code", { className: `language-${lang}` }, text);
|
||||
}
|
||||
|
||||
describe("CodeBlock 流式期间纯文本渲染", () => {
|
||||
test("流式时渲染为纯文本 pre", () => {
|
||||
const child = createCodeChild("const x = 1;");
|
||||
|
||||
renderWithProviders(createElement(CodeBlock, { children: child, isStreaming: true }));
|
||||
|
||||
const pre = document.querySelector(".code-block");
|
||||
expect(pre).toBeTruthy();
|
||||
expect(pre!.tagName).toBe("PRE");
|
||||
expect(screen.getByText("const x = 1;")).toBeTruthy();
|
||||
expect(document.querySelector(".code-block-header")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CodeBlock 非流式高亮渲染", () => {
|
||||
test("非流式时显示语言标签和复制按钮", () => {
|
||||
const child = createCodeChild("const x = 1;");
|
||||
|
||||
renderWithProviders(createElement(CodeBlock, { children: child, isStreaming: false }));
|
||||
|
||||
expect(document.querySelector(".code-block-header")).toBeTruthy();
|
||||
expect(document.querySelector(".code-block-lang")).toBeTruthy();
|
||||
expect(screen.getByText("javascript")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("非流式时异步高亮代码", async () => {
|
||||
const child = createCodeChild("const x = 1;");
|
||||
|
||||
renderWithProviders(createElement(CodeBlock, { children: child, isStreaming: false }));
|
||||
|
||||
await waitFor(() => {
|
||||
const body = document.querySelector(".code-block-body");
|
||||
expect(body).toBeTruthy();
|
||||
expect(body!.innerHTML).toContain("const x = 1;");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CodeBlock 复制按钮", () => {
|
||||
test("复制按钮存在且可点击", () => {
|
||||
const child = createCodeChild("test code");
|
||||
|
||||
renderWithProviders(createElement(CodeBlock, { children: child, isStreaming: false }));
|
||||
|
||||
const button = document.querySelector(".code-block-header button");
|
||||
expect(button).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CodeBlock 语言标签显示", () => {
|
||||
test("常见语言显示正确", () => {
|
||||
const child = createCodeChild("print('hello')", "python");
|
||||
|
||||
renderWithProviders(createElement(CodeBlock, { children: child, isStreaming: false }));
|
||||
|
||||
expect(screen.getByText("python")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("无语言时显示 text", () => {
|
||||
const child = createElement("code", { className: "" }, "plain text");
|
||||
|
||||
renderWithProviders(createElement(CodeBlock, { children: child, isStreaming: false }));
|
||||
|
||||
expect(screen.getByText("text")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
43
tests/web/components/chat/MarkdownTable.test.tsx
Normal file
43
tests/web/components/chat/MarkdownTable.test.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createElement } from "react";
|
||||
|
||||
import { MarkdownTable } from "../../../../src/web/features/chat/parts/MarkdownTable";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
describe("MarkdownTable 渲染表格", () => {
|
||||
test("渲染原生 table 元素并添加 class", () => {
|
||||
const children = createElement(
|
||||
"thead",
|
||||
null,
|
||||
createElement("tr", null, createElement("th", null, "列1"), createElement("th", null, "列2")),
|
||||
);
|
||||
|
||||
renderWithProviders(createElement(MarkdownTable, { children }));
|
||||
|
||||
const table = document.querySelector(".markdown-table");
|
||||
expect(table).toBeTruthy();
|
||||
expect(table!.tagName).toBe("TABLE");
|
||||
expect(screen.getByText("列1")).toBeTruthy();
|
||||
expect(screen.getByText("列2")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("正确传递 children 内容", () => {
|
||||
const children = createElement(
|
||||
"tbody",
|
||||
null,
|
||||
createElement("tr", null, createElement("td", null, "值1"), createElement("td", null, "值2")),
|
||||
);
|
||||
|
||||
renderWithProviders(createElement(MarkdownTable, { children }));
|
||||
|
||||
expect(screen.getByText("值1")).toBeTruthy();
|
||||
expect(screen.getByText("值2")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("只有传入 table 元素时才有 class", () => {
|
||||
const { container } = renderWithProviders(createElement(MarkdownTable, { children: null }));
|
||||
|
||||
expect(container.querySelector(".markdown-table")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
92
tests/web/components/chat/TextPart.test.tsx
Normal file
92
tests/web/components/chat/TextPart.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createElement } from "react";
|
||||
|
||||
import { TextPart } from "../../../../src/web/features/chat/parts/TextPart";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
function createTextPart(text: string, _isStreaming = false, _role = "assistant"): Record<string, unknown> {
|
||||
return { text, type: "text" };
|
||||
}
|
||||
|
||||
describe("TextPart AI 消息含 Markdown 渲染", () => {
|
||||
test("代码块使用 pre override 渲染", () => {
|
||||
const part = createTextPart("```javascript\nconst x = 1;\n```");
|
||||
|
||||
renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "assistant" }));
|
||||
|
||||
expect(screen.getByText("const x = 1;")).toBeTruthy();
|
||||
expect(document.querySelector(".code-block")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("表格使用 markdown-table class 渲染", () => {
|
||||
const part = createTextPart("| A | B |\n| --- | --- |\n| 1 | 2 |");
|
||||
|
||||
renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "assistant" }));
|
||||
|
||||
expect(screen.getByText("A")).toBeTruthy();
|
||||
expect(document.querySelector(".markdown-table")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("表格单元格内富文本正常渲染", () => {
|
||||
const part = createTextPart("| A | B |\n| --- | --- |\n| **粗体** | `code` |");
|
||||
|
||||
renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "assistant" }));
|
||||
|
||||
const bold = screen.getByText("粗体");
|
||||
expect(bold).toBeTruthy();
|
||||
expect(bold.tagName).toBe("STRONG");
|
||||
expect(screen.getByText("code")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextPart 用户消息不受 overrides 影响", () => {
|
||||
test("用户消息不应用 Markdown 渲染", () => {
|
||||
const part = createTextPart("```javascript\nconst x = 1;\n```");
|
||||
|
||||
const { container } = renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "user" }));
|
||||
|
||||
expect(container.textContent).toContain("const x = 1;");
|
||||
expect(document.querySelector(".code-block")).toBeNull();
|
||||
});
|
||||
|
||||
test("用户消息使用 Typography.Paragraph 渲染", () => {
|
||||
const part = createTextPart("普通文本消息");
|
||||
|
||||
renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "user" }));
|
||||
|
||||
expect(document.querySelector(".message-body-text")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextPart 流式状态传递", () => {
|
||||
test("流式状态下 isStreaming 传递给 CodeBlock", () => {
|
||||
const part = createTextPart("```javascript\nconst x = 1;\n```");
|
||||
|
||||
renderWithProviders(createElement(TextPart, { isStreaming: true, part, role: "assistant" }));
|
||||
|
||||
const codeBlock = document.querySelector(".code-block");
|
||||
expect(codeBlock).toBeTruthy();
|
||||
expect(codeBlock!.tagName).toBe("PRE");
|
||||
});
|
||||
|
||||
test("非流式状态下渲染完整代码块结构", () => {
|
||||
const part = createTextPart("```javascript\nconst x = 1;\n```");
|
||||
|
||||
renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "assistant" }));
|
||||
|
||||
expect(document.querySelector(".code-block-header")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextPart 纯文本 AI 消息", () => {
|
||||
test("无代码块无表格的消息正常渲染", () => {
|
||||
const part = createTextPart("这是一条普通的 AI 回复。");
|
||||
|
||||
const { container } = renderWithProviders(createElement(TextPart, { isStreaming: false, part, role: "assistant" }));
|
||||
|
||||
expect(screen.getByText("这是一条普通的 AI 回复。")).toBeTruthy();
|
||||
expect(container.querySelector(".code-block")).toBeNull();
|
||||
expect(container.querySelector(".markdown-table")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user