feat: 聊天室对话渲染增强 - 思考内容Markdown渲染 + 工具调用参数卡片化
This commit is contained in:
97
tests/web/components/chat/HighlightBlock.test.tsx
Normal file
97
tests/web/components/chat/HighlightBlock.test.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { createElement } from "react";
|
||||
|
||||
import { HighlightBlock } from "../../../../src/web/features/chat/parts/HighlightBlock";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
describe("HighlightBlock JSON 高亮", () => {
|
||||
test("非流式状态渲染 shiki 高亮 HTML", async () => {
|
||||
const code = JSON.stringify({ key: "value" }, null, 2);
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
createElement(HighlightBlock, { code, isStreaming: false, lang: "json" }),
|
||||
);
|
||||
|
||||
expect(screen.getByText("json")).toBeTruthy();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".code-block-body")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test("流式状态渲染纯 <pre><code> 无高亮", () => {
|
||||
const code = JSON.stringify({ key: "value" }, null, 2);
|
||||
|
||||
const { container } = renderWithProviders(createElement(HighlightBlock, { code, isStreaming: true, lang: "json" }));
|
||||
|
||||
const pre = container.querySelector("pre.code-block");
|
||||
expect(pre).toBeTruthy();
|
||||
expect(pre!.textContent).toContain("key");
|
||||
expect(container.querySelector(".code-block-header")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HighlightBlock 复制按钮", () => {
|
||||
test("非流式状态显示复制按钮", () => {
|
||||
const code = "const x = 1;";
|
||||
|
||||
renderWithProviders(createElement(HighlightBlock, { code, isStreaming: false, lang: "text" }));
|
||||
|
||||
expect(screen.getByText("text")).toBeTruthy();
|
||||
const buttons = screen.getAllByRole("button");
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("复制按钮调用 clipboard.writeText", () => {
|
||||
const code = JSON.stringify({ a: 1 }, null, 2);
|
||||
const writeTextMock = mock(() => Promise.resolve());
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: writeTextMock },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
renderWithProviders(createElement(HighlightBlock, { code, isStreaming: false, lang: "json" }));
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
button.click();
|
||||
|
||||
expect(writeTextMock).toHaveBeenCalledWith(code);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HighlightBlock 纯文本", () => {
|
||||
test("lang=text 时头部显示 text", () => {
|
||||
renderWithProviders(createElement(HighlightBlock, { code: "hello world", isStreaming: false, lang: "text" }));
|
||||
|
||||
expect(screen.getByText("text")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("HighlightBlock 边界情况", () => {
|
||||
test("code 为空时渲染空代码块不触发异步高亮", () => {
|
||||
const { container } = renderWithProviders(
|
||||
createElement(HighlightBlock, { code: "", isStreaming: false, lang: "json" }),
|
||||
);
|
||||
|
||||
expect(container.querySelector(".code-block")).toBeTruthy();
|
||||
expect(container.querySelector("code")).toBeTruthy();
|
||||
expect(container.querySelector("code")!.textContent).toBe("");
|
||||
});
|
||||
|
||||
test("流式切换到非流式后触发高亮", async () => {
|
||||
const code = JSON.stringify({ x: 1 }, null, 2);
|
||||
const { container, rerender } = renderWithProviders(
|
||||
createElement(HighlightBlock, { code, isStreaming: true, lang: "json" }),
|
||||
);
|
||||
|
||||
expect(container.querySelector("pre.code-block")).toBeTruthy();
|
||||
|
||||
rerender(createElement(HighlightBlock, { code, isStreaming: false, lang: "json" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".code-block-body")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,4 +46,32 @@ describe("ReasoningPart", () => {
|
||||
|
||||
expect(screen.getByText("思考中内容")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Markdown 代码块渲染(流式状态展开)", () => {
|
||||
const part = { state: "streaming", text: "```typescript\nconst x = 1;\n```", type: "reasoning" };
|
||||
|
||||
renderWithProviders(createElement(ReasoningPart, { part }));
|
||||
|
||||
expect(screen.getByText("const x = 1;")).toBeTruthy();
|
||||
expect(document.querySelector(".code-block")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Markdown 有序列表渲染(流式状态展开)", () => {
|
||||
const part = { state: "streaming", text: "1. 第一项\n2. 第二项", type: "reasoning" };
|
||||
|
||||
const { container } = renderWithProviders(createElement(ReasoningPart, { part }));
|
||||
|
||||
const ol = container.querySelector("ol");
|
||||
expect(ol).toBeTruthy();
|
||||
expect(screen.getByText("第一项")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Markdown 表格渲染(流式状态展开)", () => {
|
||||
const part = { state: "streaming", text: "| A | B |\n| --- | --- |\n| 1 | 2 |", type: "reasoning" };
|
||||
|
||||
renderWithProviders(createElement(ReasoningPart, { part }));
|
||||
|
||||
expect(screen.getByText("A")).toBeTruthy();
|
||||
expect(document.querySelector(".markdown-table")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { createElement } from "react";
|
||||
|
||||
import { ToolPart } from "../../../../src/web/features/chat/parts/ToolPart";
|
||||
@@ -63,3 +63,72 @@ describe("ToolPart 工具显示名", () => {
|
||||
expect(screen.getByText(/获取当前时间.*失败/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolPart 入参/出参分层展示", () => {
|
||||
test("输入流式状态时面板展开显示入参区块标题", () => {
|
||||
const part = {
|
||||
input: { timezone: "Asia/Shanghai" },
|
||||
toolCallId: "call-stream",
|
||||
toolName: "getCurrentTime",
|
||||
type: "tool-getCurrentTime",
|
||||
};
|
||||
|
||||
renderWithProviders(createElement(ToolPart, { part }));
|
||||
|
||||
const inputLabels = screen.getAllByText("入参");
|
||||
expect(inputLabels.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("点击面板展开后显示入参出参区块", () => {
|
||||
const part = {
|
||||
input: { timezone: "Asia/Shanghai" },
|
||||
output: { iso: "2024-01-01T00:00:00.000Z" },
|
||||
toolCallId: "call-1",
|
||||
toolName: "getCurrentTime",
|
||||
type: "tool-getCurrentTime",
|
||||
};
|
||||
|
||||
const { container } = renderWithProviders(createElement(ToolPart, { part }));
|
||||
|
||||
const header = container.querySelector(".ant-collapse-header");
|
||||
expect(header).toBeTruthy();
|
||||
if (header) fireEvent.click(header);
|
||||
|
||||
expect(screen.getByText("入参")).toBeTruthy();
|
||||
expect(screen.getByText("出参")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("输入流式状态时显示生成中文字", () => {
|
||||
const part = {
|
||||
toolCallId: "call-stream",
|
||||
toolName: "getCurrentTime",
|
||||
type: "tool-getCurrentTime",
|
||||
};
|
||||
|
||||
renderWithProviders(createElement(ToolPart, { part }));
|
||||
|
||||
expect(screen.getByText("生成中...")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolPart 错误区块", () => {
|
||||
test("点击错误面板展开后显示错误标题和错误文本(由 HighlightBlock 渲染)", () => {
|
||||
const part = {
|
||||
errorText: "网络超时",
|
||||
toolCallId: "call-err",
|
||||
toolName: "getCurrentTime",
|
||||
type: "tool-getCurrentTime",
|
||||
};
|
||||
|
||||
const { container } = renderWithProviders(createElement(ToolPart, { part }));
|
||||
|
||||
const header = container.querySelector(".ant-collapse-header");
|
||||
expect(header).toBeTruthy();
|
||||
if (header) fireEvent.click(header);
|
||||
|
||||
expect(screen.getByText("错误")).toBeTruthy();
|
||||
expect(screen.getByText("网络超时")).toBeTruthy();
|
||||
// 错误文本在 HighlightBlock 中渲染,带 code-block 结构
|
||||
expect(container.querySelector(".code-block")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user