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 ( {children} ); } 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( ); 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( ); 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( ); 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( ); 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( ); 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", }); }); }); });