import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import React, { type ReactNode } from "react"; import { MoveEmbedPickerDialog } from "./move-embed-picker-dialog"; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const sidebarData = { activeWorkspaceId: "ws_test", workspaces: [], documents: [], kernelSidebarProjection: { projectionId: "kernel_projection:sidebar_tree:workspace_root", projection: "sidebar_tree", rootNodeId: null, items: [], edges: [], }, kernelSidebarTree: [], trashedDocuments: [], }; function buildSidebarNode(input: { id: string; title: string; children?: Array>; }) { return { access_scope: "private", id: input.id, workspace_id: "ws_test", title: input.title, parent_id: null, sort_order: 0, is_starred: false, is_template: false, created_at: "2026-04-24T00:00:00Z", updated_at: "2026-04-24T00:00:00Z", children: input.children ?? [], kernel: { nodeType: "page" as const, depth: 0, position: 0, childCount: input.children?.length ?? 0, expandedByDefault: true, }, }; } vi.mock("@tanstack/react-query", () => ({ useQuery: ({ queryKey }: { queryKey: unknown[] }) => { const key = Array.isArray(queryKey) ? queryKey[0] : queryKey; if (key === "move-embed-picker-sidebar") { return { data: sidebarData, isLoading: false, error: null, }; } return { data: null, isLoading: false, error: null, }; }, })); const mockUseDocumentSearch = vi.fn(() => ({ data: null, isLoading: false, error: null, })); vi.mock("@/hooks/use-document-search", () => ({ useDocumentSearch: (...args: unknown[]) => mockUseDocumentSearch(...args), })); vi.mock("@/components/ui/dialog", () => ({ Dialog: ({ open, children }: { open: boolean; children: ReactNode }) => (open ?
{children}
: null), DialogContent: ({ children, className }: { children: ReactNode; className?: string }) => (
{children}
), DialogTitle: ({ children, className }: { children: ReactNode; className?: string }) => (
{children}
), })); vi.mock("@/components/ui/input", () => ({ Input: React.forwardRef>((props, ref) => ( )), })); vi.mock("@/components/ui/tabs", () => ({ Tabs: ({ children }: { children: ReactNode }) =>
{children}
, TabsList: ({ children, className }: { children: ReactNode; className?: string }) => (
{children}
), TabsTrigger: ({ children, className, value, }: { children: ReactNode; className?: string; value: string; }) => ( ), TabsContent: ({ children, className }: { children: ReactNode; className?: string }) => (
{children}
), })); describe("MoveEmbedPickerDialog", () => { let container: HTMLDivElement; let root: Root; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); window.__MNOTE_RUNTIME_CONFIG__ = { treeRendererFamily: "react", }; }); afterEach(() => { act(() => { root.unmount(); }); container.remove(); vi.restoreAllMocks(); vi.clearAllMocks(); mockUseDocumentSearch.mockReset(); mockUseDocumentSearch.mockReturnValue({ data: null, isLoading: false, error: null, }); sidebarData.kernelSidebarTree = []; delete window.__MNOTE_RUNTIME_CONFIG__; }); it("空查询时会使用统一 picker surface 并透传根目录选择", async () => { const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); const shellPickButton = container.querySelector('[data-testid="tree-picker-root"]'); expect(shellPickButton).not.toBeNull(); await act(async () => { shellPickButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onPick).toHaveBeenCalledTimes(1); expect(onPick).toHaveBeenCalledWith("move", null); expect(onOpenChange).toHaveBeenCalledWith(false); }); it("空查询时应保留根节点并过滤 excludeIds", async () => { sidebarData.kernelSidebarTree = [ buildSidebarNode({ id: "doc_hidden", title: "隐藏页面" }), buildSidebarNode({ id: "doc_visible", title: "保留页面" }), ]; await act(async () => { root.render( undefined)} />, ); }); expect(container.querySelector('[data-testid="tree-picker-root"]')).not.toBeNull(); expect(container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_hidden"]')).toBeNull(); expect(container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_visible"]')).not.toBeNull(); }); it("空查询态应支持键盘高亮切换并按当前高亮项选中", async () => { sidebarData.kernelSidebarTree = [ buildSidebarNode({ id: "doc_first", title: "第一页" }), buildSidebarNode({ id: "doc_second", title: "第二页" }), ]; const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); const input = container.querySelector("input"); const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]'); expect(input).not.toBeNull(); expect(pickerRows).toHaveLength(2); await act(async () => { input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })); }); expect(pickerRows[0]?.className ?? "").toContain("bg-[#e3ecff]"); await act(async () => { input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); }); expect(onPick).toHaveBeenCalledWith("move", "doc_first"); expect(onOpenChange).toHaveBeenCalledWith(false); }); it("搜索结果也应继续复用统一 picker surface", async () => { mockUseDocumentSearch.mockReturnValue({ data: { results: [ { id: "doc_target", title: "目标页面", matchField: "title", }, ], }, isLoading: false, error: null, }); const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); const input = container.querySelector("input"); expect(input).not.toBeNull(); await act(async () => { input?.dispatchEvent(new Event("input", { bubbles: true })); Object.defineProperty(input as HTMLInputElement, "value", { configurable: true, value: "目标", }); input?.dispatchEvent(new Event("change", { bubbles: true })); }); const pickerSurface = container.querySelector('[data-testid="tree-picker-surface"]'); const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]'); expect(pickerSurface).not.toBeNull(); expect(pickerRows).toHaveLength(1); await act(async () => { pickerRows[0]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onPick).toHaveBeenCalledWith("move", "doc_target"); expect(onOpenChange).toHaveBeenCalledWith(false); }); it("搜索结果态应支持高亮切换并按当前高亮项选中", async () => { mockUseDocumentSearch.mockReturnValue({ data: { results: [ { id: "doc_first", title: "第一页", matchField: "title", }, { id: "doc_second", title: "第二页", matchField: "title", }, ], }, isLoading: false, error: null, }); const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); const input = container.querySelector("input"); expect(input).not.toBeNull(); await act(async () => { Object.defineProperty(input as HTMLInputElement, "value", { configurable: true, value: "第", }); input?.dispatchEvent(new Event("input", { bubbles: true })); input?.dispatchEvent(new Event("change", { bubbles: true })); }); const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]'); expect(pickerRows).toHaveLength(2); await act(async () => { pickerRows[1]?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true })); }); expect(pickerRows[1]?.className ?? "").toContain("bg-[#e3ecff]"); await act(async () => { pickerRows[1]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onPick).toHaveBeenCalledWith("move", "doc_second"); expect(onOpenChange).toHaveBeenCalledWith(false); }); it("搜索结果态不应再注入根节点,且仍应支持多次 ArrowDown 后选中后续结果", async () => { mockUseDocumentSearch.mockReturnValue({ data: { results: [ { id: "doc_first", title: "第一页", matchField: "title", }, { id: "doc_second", title: "第二页", matchField: "title", }, ], }, isLoading: false, error: null, }); const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); const input = container.querySelector("input"); expect(input).not.toBeNull(); await act(async () => { Object.defineProperty(input as HTMLInputElement, "value", { configurable: true, value: "第", }); input?.dispatchEvent(new Event("input", { bubbles: true })); input?.dispatchEvent(new Event("change", { bubbles: true })); }); const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]'); expect(container.querySelector('[data-testid="tree-picker-root"]')).toBeNull(); expect(pickerRows).toHaveLength(2); await act(async () => { input?.focus(); input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })); }); await act(async () => { input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })); }); await act(async () => { input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); }); expect(onPick).toHaveBeenCalledWith("move", "doc_second"); expect(onOpenChange).toHaveBeenCalledWith(false); }); it("rust_family 配置下,picker 空态与结果态都应进入统一 host", async () => { window.__MNOTE_RUNTIME_CONFIG__ = { treeRendererFamily: "rust_family", }; const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); const emptySurface = container.querySelector('[data-testid="tree-picker-surface"]'); expect(emptySurface?.getAttribute("data-tree-host-kind")).toBe("rust_family"); expect(emptySurface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host"); expect( container.querySelector('[data-testid="tree-picker-surface-rust-host"]'), ).not.toBeNull(); expect( container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'), ).not.toBeNull(); mockUseDocumentSearch.mockReturnValue({ data: { results: [ { id: "doc_target", title: "目标页面", matchField: "title", }, ], }, isLoading: false, error: null, }); const input = container.querySelector("input"); expect(input).not.toBeNull(); await act(async () => { Object.defineProperty(input as HTMLInputElement, "value", { configurable: true, value: "目标", }); input?.dispatchEvent(new Event("input", { bubbles: true })); input?.dispatchEvent(new Event("change", { bubbles: true })); }); const resultSurface = container.querySelector('[data-testid="tree-picker-surface"]'); expect(resultSurface?.getAttribute("data-tree-host-kind")).toBe("rust_family"); expect(resultSurface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host"); expect( container.querySelector('[data-testid="tree-picker-surface-rust-host"]'), ).not.toBeNull(); expect( container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'), ).not.toBeNull(); }); it("rust_family 配置下,输入框键盘命令应转发给 iframe,并用焦点回传更新 shell 状态", async () => { window.__MNOTE_RUNTIME_CONFIG__ = { treeRendererFamily: "rust_family", }; mockUseDocumentSearch.mockReturnValue({ data: { results: [ { id: "doc_first", title: "第一页", matchField: "title", }, { id: "doc_second", title: "第二页", matchField: "title", }, ], }, isLoading: false, error: null, }); vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( '', { status: 200, headers: { "content-type": "text/html" } }, ), ); await act(async () => { root.render( undefined)} />, ); }); const input = container.querySelector("input"); expect(input).not.toBeNull(); await act(async () => { Object.defineProperty(input as HTMLInputElement, "value", { configurable: true, value: "第", }); input?.dispatchEvent(new Event("input", { bubbles: true })); input?.dispatchEvent(new Event("change", { bubbles: true })); }); const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'); expect(iframe).not.toBeNull(); Object.defineProperty(iframe, "contentWindow", { configurable: true, value: window, }); const postMessageMock = vi.spyOn(window, "postMessage").mockImplementation(() => undefined); postMessageMock.mockClear(); await act(async () => { input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })); }); expect(postMessageMock).toHaveBeenCalledWith( expect.objectContaining({ channel: "tree-picker-surface", type: "tree.picker.command", command: "next", }), "*", ); postMessageMock.mockClear(); await act(async () => { window.dispatchEvent( new MessageEvent("message", { data: { channel: "tree-picker-surface", type: "tree.picker.focus.changed", documentId: "doc_second", itemKey: "doc_second", }, source: window, }), ); }); expect(postMessageMock).toHaveBeenCalledWith( expect.objectContaining({ channel: "tree-picker-surface", type: "tree.shell.state.patch", activeDocumentId: "doc_second", activePickerItemKey: "doc_second", }), "*", ); postMessageMock.mockClear(); await act(async () => { input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); }); expect(postMessageMock).toHaveBeenCalledWith( expect.objectContaining({ channel: "tree-picker-surface", type: "tree.picker.command", command: "pick", }), "*", ); }); it("rust_family 配置下,无根目录且无结果时也应保持 same-origin host 空态", async () => { window.__MNOTE_RUNTIME_CONFIG__ = { treeRendererFamily: "rust_family", }; vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( '', { status: 200, headers: { "content-type": "text/html" } }, ), ); const onPick = vi.fn(async () => undefined); const onOpenChange = vi.fn(); await act(async () => { root.render( , ); }); await act(async () => { await Promise.resolve(); }); const surface = container.querySelector('[data-testid="tree-picker-surface"]'); const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'); expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family"); expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host"); expect(iframe).not.toBeNull(); expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1"); }); });