116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
/* eslint-disable @next/next/no-img-element */
|
|
import "@testing-library/jest-dom";
|
|
import React from "react";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
|
import { AgentTurn } from "./AgentTurn";
|
|
|
|
jest.mock("next/image", () => ({
|
|
__esModule: true,
|
|
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
|
|
<img {...props} alt={props.alt ?? ""} />
|
|
),
|
|
}));
|
|
|
|
jest.mock("framer-motion", () => ({
|
|
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
motion: {
|
|
div: ({
|
|
children,
|
|
animate: _animate,
|
|
exit: _exit,
|
|
initial: _initial,
|
|
transition: _transition,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement> & Record<string, unknown>) => (
|
|
<div {...props}>{children}</div>
|
|
),
|
|
span: ({
|
|
children,
|
|
animate: _animate,
|
|
transition: _transition,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLSpanElement> & Record<string, unknown>) => (
|
|
<span {...props}>{children}</span>
|
|
),
|
|
},
|
|
}));
|
|
|
|
jest.mock("./AgentMarkdownBlock", () => ({
|
|
MarkdownBlock: ({ children }: { children: string }) => (
|
|
<div>{children.split(/\n+/u).map((line) => <p key={line}>{line}</p>)}</div>
|
|
),
|
|
normalizeClipboardText: (value: string) => value.replace(/\s+$/u, ""),
|
|
}));
|
|
|
|
describe("AgentTurn speech selection", () => {
|
|
it("shows a floating action and reads from the selected text", async () => {
|
|
const content = "第一段内容。\n\n第二段内容。";
|
|
const speechText = "第一段内容。\n第二段内容。";
|
|
const onSpeak = jest.fn();
|
|
const removeAllRanges = jest.fn();
|
|
|
|
render(
|
|
<AgentTurn
|
|
message={{ id: "assistant-1", role: "assistant", content }}
|
|
isStreaming={false}
|
|
messageSpeechState="idle"
|
|
onSpeak={onSpeak}
|
|
onPause={jest.fn()}
|
|
onResume={jest.fn()}
|
|
onStopSpeech={jest.fn()}
|
|
isTtsSupported
|
|
onCreateBranch={jest.fn()}
|
|
onReplyPermission={jest.fn()}
|
|
onReplyQuestion={jest.fn()}
|
|
onRejectQuestion={jest.fn()}
|
|
/>,
|
|
);
|
|
|
|
const selectedParagraph = screen.getByText("第二段内容。");
|
|
const selectedTextNode = selectedParagraph.firstChild as Text;
|
|
const range = {
|
|
commonAncestorContainer: selectedTextNode,
|
|
getBoundingClientRect: () => ({
|
|
width: 72,
|
|
height: 20,
|
|
top: 120,
|
|
right: 172,
|
|
bottom: 140,
|
|
left: 100,
|
|
x: 100,
|
|
y: 120,
|
|
toJSON: () => ({}),
|
|
}),
|
|
} as unknown as Range;
|
|
const selection = {
|
|
rangeCount: 1,
|
|
isCollapsed: false,
|
|
getRangeAt: () => range,
|
|
toString: () => "第二段",
|
|
removeAllRanges,
|
|
} as unknown as Selection;
|
|
|
|
jest.spyOn(window, "getSelection").mockReturnValue(selection);
|
|
jest.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
|
callback(0);
|
|
return 1;
|
|
});
|
|
|
|
fireEvent.pointerUp(selectedParagraph);
|
|
|
|
const speechAction = await screen.findByRole("button", { name: "从这里开始朗读" });
|
|
fireEvent.click(speechAction);
|
|
|
|
await waitFor(() => {
|
|
expect(onSpeak).toHaveBeenCalledWith("assistant-1", speechText, {
|
|
startOffset: speechText.indexOf("第二段"),
|
|
});
|
|
});
|
|
expect(removeAllRanges).toHaveBeenCalledTimes(1);
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole("button", { name: "从这里开始朗读" })).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|