You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
5.0 KiB
178 lines
5.0 KiB
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { act } from "react";
|
|
import { ReactFlowProvider } from "@xyflow/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { SmartTextNode } from "@/components/nodes/SmartTextNode";
|
|
import { REQUEST_SMART_TEXT_EDIT_EVENT } from "@/utils/smartTextEditRequest";
|
|
|
|
const mockUpdateNodeData = vi.fn();
|
|
|
|
vi.mock("@/store/workflowStore", () => ({
|
|
useWorkflowStore: vi.fn((selector) => {
|
|
const state = {
|
|
updateNodeData: mockUpdateNodeData,
|
|
edges: [],
|
|
currentNodeIds: [],
|
|
groups: {},
|
|
nodes: [],
|
|
getConnectedInputs: vi.fn(() => ({ images: [], videos: [], text: null, dynamicInputs: {} })),
|
|
getNodesWithComments: vi.fn(() => []),
|
|
markCommentViewed: vi.fn(),
|
|
setNavigationTarget: vi.fn(),
|
|
setHoveredNodeId: vi.fn(),
|
|
};
|
|
return selector(state);
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/hooks/useSelectedNodeCount", () => ({
|
|
useSelectedNodeCount: () => 1,
|
|
}));
|
|
|
|
function TestWrapper({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<ReactFlowProvider>
|
|
{children}
|
|
</ReactFlowProvider>
|
|
);
|
|
}
|
|
|
|
const defaultProps = {
|
|
id: "text-1",
|
|
type: "smartText" as const,
|
|
data: {
|
|
manualLocked: true,
|
|
manualText: "First line\nLast character",
|
|
inputPrompt: "",
|
|
inputImages: [],
|
|
outputText: null,
|
|
provider: "openai" as const,
|
|
model: "gpt-4o-mini",
|
|
temperature: 0.7,
|
|
maxTokens: 1000,
|
|
status: "idle" as const,
|
|
error: null,
|
|
},
|
|
selected: true,
|
|
isConnectable: true,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
zIndex: 0,
|
|
draggable: true,
|
|
dragging: false,
|
|
deletable: true,
|
|
selectable: true,
|
|
parentId: undefined,
|
|
dragHandle: undefined,
|
|
};
|
|
|
|
describe("SmartTextNode", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("enters editing when an external smart text edit request targets the node", async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<SmartTextNode {...defaultProps} />
|
|
</TestWrapper>
|
|
);
|
|
|
|
const renderedText = document.querySelector("p");
|
|
expect(renderedText?.textContent).toContain("Last character");
|
|
|
|
act(() => {
|
|
window.dispatchEvent(new CustomEvent(REQUEST_SMART_TEXT_EDIT_EVENT, {
|
|
detail: { nodeId: "text-1" },
|
|
}));
|
|
});
|
|
|
|
const editor = await screen.findByRole("textbox", { name: /Write text content|编写文本内容|編寫文字內容|テキスト内容を書く/ });
|
|
await waitFor(() => expect(document.activeElement).toBe(editor));
|
|
|
|
const selection = window.getSelection();
|
|
expect(selection?.rangeCount).toBe(1);
|
|
expect(selection?.getRangeAt(0).collapsed).toBe(true);
|
|
});
|
|
|
|
it("prevents native text selection when double-clicking preview text", () => {
|
|
render(
|
|
<TestWrapper>
|
|
<SmartTextNode {...defaultProps} />
|
|
</TestWrapper>
|
|
);
|
|
|
|
const renderedText = document.querySelector("p");
|
|
expect(renderedText?.textContent).toContain("Last character");
|
|
|
|
const mouseDown = new MouseEvent("mousedown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
detail: 2,
|
|
});
|
|
|
|
renderedText!.dispatchEvent(mouseDown);
|
|
|
|
expect(mouseDown.defaultPrevented).toBe(true);
|
|
});
|
|
|
|
it("ignores external smart text edit requests for other nodes", () => {
|
|
render(
|
|
<TestWrapper>
|
|
<SmartTextNode {...defaultProps} />
|
|
</TestWrapper>
|
|
);
|
|
|
|
act(() => {
|
|
window.dispatchEvent(new CustomEvent(REQUEST_SMART_TEXT_EDIT_EVENT, {
|
|
detail: { nodeId: "other-text-node" },
|
|
}));
|
|
});
|
|
|
|
expect(screen.queryByRole("textbox", { name: /Write text content|编写文本内容|編寫文字內容|テキスト内容を書く/ })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("keeps the editable surface isolated from canvas drag and pan interactions", async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<SmartTextNode {...defaultProps} />
|
|
</TestWrapper>
|
|
);
|
|
|
|
act(() => {
|
|
window.dispatchEvent(new CustomEvent(REQUEST_SMART_TEXT_EDIT_EVENT, {
|
|
detail: { nodeId: "text-1" },
|
|
}));
|
|
});
|
|
|
|
const editor = await screen.findByRole("textbox", { name: /Write text content|编写文本内容|編寫文字內容|テキスト内容を書く/ });
|
|
expect(editor).toHaveClass("nodrag");
|
|
expect(editor).toHaveClass("nopan");
|
|
expect(editor).toHaveClass("nowheel");
|
|
});
|
|
|
|
it("commits edits on native blur", async () => {
|
|
render(
|
|
<TestWrapper>
|
|
<SmartTextNode {...defaultProps} />
|
|
</TestWrapper>
|
|
);
|
|
|
|
act(() => {
|
|
window.dispatchEvent(new CustomEvent(REQUEST_SMART_TEXT_EDIT_EVENT, {
|
|
detail: { nodeId: "text-1" },
|
|
}));
|
|
});
|
|
const editor = await screen.findByRole("textbox", { name: /Write text content|编写文本内容|編寫文字內容|テキスト内容を書く/ });
|
|
|
|
editor.innerHTML = "Updated text";
|
|
fireEvent.input(editor);
|
|
fireEvent.blur(editor);
|
|
|
|
await waitFor(() => {
|
|
expect(mockUpdateNodeData).toHaveBeenCalledWith("text-1", {
|
|
manualText: "Updated text",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|