Files
mnote/wolai-frontend/src/components/sidebar/tree-shell-surface.test.tsx
T

267 lines
8.8 KiB
TypeScript
Raw Normal View History

2026-04-24 06:10:18 +08:00
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SidebarTreeSurface, TreePickerSurface, type TreeRendererFamily } from "./tree-shell-surface";
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
vi.mock("@/components/sidebar/private-tree", () => ({
PrivateTree: () => <div data-testid="private-tree-fallback" />,
}));
vi.mock("@/components/sidebar/file-tree", () => ({
FileTree: () => <div data-testid="file-tree-fallback" />,
}));
describe("tree-shell-surface", () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root.unmount();
});
container.remove();
});
function renderPageSurface(rendererFamily: TreeRendererFamily) {
act(() => {
root.render(
<SidebarTreeSurface
mode="page"
rendererFamily={rendererFamily}
workspaceId="ws_1"
treeShellEnabled
rows={[]}
expanded={new Set<string>()}
activeId=""
onToggleExpand={() => undefined}
onMove={() => undefined}
onCreateChild={() => undefined}
onContextMenu={() => undefined}
/>,
);
});
}
it("page tree surface 在 rust_family 下可切到同源 iframe host", () => {
renderPageSurface("rust_family");
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
const rustHost = container.querySelector(
'[data-testid="sidebar-page-tree-shell-rust-host"]',
);
const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
expect(rustHost).not.toBeNull();
expect(iframe).not.toBeNull();
expect(container.querySelector('[data-testid="private-tree-fallback"]')).toBeNull();
});
it("page tree 在禁用 tree shell 时应安全回退到 React fallback", () => {
act(() => {
root.render(
<SidebarTreeSurface
mode="page"
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled={false}
rows={[]}
expanded={new Set<string>()}
activeId=""
onToggleExpand={() => undefined}
onMove={() => undefined}
onCreateChild={() => undefined}
onContextMenu={() => undefined}
/>,
);
});
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("react_fallback");
expect(container.querySelector('[data-testid="private-tree-fallback"]')).not.toBeNull();
});
it("file tree surface 在 rust_family 下也应走同一 host 选择契约", () => {
act(() => {
root.render(
<SidebarTreeSurface
mode="filetree"
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled
rows={[]}
activeId=""
selectedRowIds={new Set<string>()}
onRowClick={() => undefined}
onRowDoubleClick={() => undefined}
onRowContextMenu={() => undefined}
onToggleExpand={() => undefined}
onCreateChild={() => undefined}
/>,
);
});
const surface = container.querySelector('[data-testid="sidebar-file-tree-shell"]');
const rustHost = container.querySelector(
'[data-testid="sidebar-file-tree-shell-rust-host"]',
);
const iframe = container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
expect(rustHost).not.toBeNull();
expect(iframe).not.toBeNull();
expect(container.querySelector('[data-testid="file-tree-fallback"]')).toBeNull();
});
it("file tree surface 应把 iframe 内部拖放与外部文件拖放桥接回宿主回调", async () => {
const onInternalDrop = vi.fn();
const onDropFiles = vi.fn();
const targetRow = {
kind: "doc",
rowId: "doc:doc_target",
depth: 0,
docId: "doc_target",
parentDocId: null,
node: {
_id: "doc_target",
id: "doc_target",
title: "目标页面",
},
hasChildren: false,
isExpanded: false,
};
const droppedFile = new File(["bridge"], "bridge.txt", { type: "text/plain" });
await act(async () => {
root.render(
<SidebarTreeSurface
mode="filetree"
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled
rows={[targetRow]}
activeId=""
selectedRowIds={new Set<string>()}
onRowClick={() => undefined}
onRowDoubleClick={() => undefined}
onRowContextMenu={() => undefined}
onToggleExpand={() => undefined}
onCreateChild={() => undefined}
onInternalDrop={onInternalDrop}
onDropFiles={onDropFiles}
/>,
);
});
const iframe = container.querySelector(
'[data-testid="sidebar-file-tree-shell-rust-iframe"]',
);
expect(iframe).not.toBeNull();
Object.defineProperty(iframe, "contentWindow", {
configurable: true,
value: window,
});
await act(async () => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
channel: "sidebar-file-tree-shell",
type: "tree.filetree.internal-drop",
rowIds: ["doc:doc_source"],
copy: false,
targetRow,
},
source: window,
}),
);
window.dispatchEvent(
new MessageEvent("message", {
data: {
channel: "sidebar-file-tree-shell",
type: "tree.filetree.external-drop",
documentId: "doc_target",
targetRow,
files: [droppedFile],
},
source: window,
}),
);
});
expect(onInternalDrop).toHaveBeenCalledWith({
targetRow,
rowIds: ["doc:doc_source"],
copy: false,
});
expect(onDropFiles).toHaveBeenCalledWith("doc_target", [droppedFile], targetRow);
});
it("picker surface 在 rust_family 下也应挂到同一 host 边界", async () => {
const onPick = vi.fn();
await act(async () => {
root.render(
<TreePickerSurface
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled
items={[{ kind: "doc", id: "doc_1", title: "页面 1", depth: 0 }]}
highlighted={0}
onHighlight={() => undefined}
onPick={onPick}
/>,
);
});
const surface = container.querySelector('[data-testid="tree-picker-surface"]');
const rustHost = container.querySelector('[data-testid="tree-picker-surface-rust-host"]');
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
expect(rustHost).not.toBeNull();
expect(iframe).not.toBeNull();
expect(onPick).not.toHaveBeenCalled();
});
it("picker 在 rust_family 但 tree shell 不可用时仍应保留 React fallback", () => {
act(() => {
root.render(
<TreePickerSurface
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled={false}
items={[{ kind: "doc", id: "doc_1", title: "页面 1", depth: 0 }]}
highlighted={0}
emptyText="没有匹配结果"
onHighlight={() => undefined}
onPick={vi.fn()}
/>,
);
});
const surface = container.querySelector('[data-testid="tree-picker-surface"]');
const rustHost = container.querySelector('[data-testid="tree-picker-surface-rust-host"]');
const row = container.querySelector('[data-testid="tree-picker-row"]');
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("react_fallback");
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
expect(rustHost).not.toBeNull();
expect(row).not.toBeNull();
});
});