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

357 lines
13 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;
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();
});
2026-04-26 04:29:23 +08:00
function renderPageSurface(rendererFamily: TreeRendererFamily, focusedDocumentId?: string) {
2026-04-24 06:10:18 +08:00
act(() => {
root.render(
<SidebarTreeSurface
mode="page"
rendererFamily={rendererFamily}
workspaceId="ws_1"
treeShellEnabled
rows={[]}
expanded={new Set<string>()}
activeId=""
2026-04-26 04:29:23 +08:00
focusedDocumentId={focusedDocumentId}
2026-04-24 06:10:18 +08:00
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");
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
expect(rustHost?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
2026-04-24 06:10:18 +08:00
expect(rustHost).not.toBeNull();
expect(iframe).not.toBeNull();
expect(iframe?.getAttribute("srcdoc")).toContain('"runtimeArtifact"');
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_tree_shell_runtime_artifact_v1"');
2026-04-26 04:29:23 +08:00
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).toBeNull();
2026-04-24 06:10:18 +08:00
});
2026-04-26 19:35:52 +08:00
it("page tree surface 在 rust_family 下应把 focusedDocumentId 作为宿主状态暴露,并使用 postMessage patch 同步 iframe", () => {
2026-04-26 04:29:23 +08:00
renderPageSurface("rust_family", "doc_focus");
const iframe = container.querySelector(
'[data-testid="sidebar-page-tree-shell-rust-iframe"]',
) as HTMLIFrameElement | null;
2026-04-26 19:35:52 +08:00
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
2026-04-26 04:29:23 +08:00
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-page-tree-focused-id")).toBe("doc_focus");
expect(iframe?.getAttribute("src")).toBeNull();
expect(iframe?.getAttribute("srcdoc")).toContain('"focusedDocumentId":"doc_focus"');
2026-04-26 04:29:23 +08:00
});
it("page tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
await act(async () => {
2026-04-24 06:10:18 +08:00
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"]');
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
2026-04-26 04:29:23 +08:00
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).not.toBeNull();
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).toBeNull();
});
it("page tree 在缺少 workspaceId 时应显示 Rust 宿主占位,而不是回退旧 React renderer", () => {
act(() => {
root.render(
<SidebarTreeSurface
mode="page"
rendererFamily="rust_family"
workspaceId={null}
treeShellEnabled
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("page_tree_renderer_removed");
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).not.toBeNull();
2026-04-24 06:10:18 +08:00
});
it("file tree surface 在 rust_family 下也应走同一 host 选择契约", () => {
act(() => {
root.render(
<SidebarTreeSurface
mode="filetree"
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled
rows={[]}
activeId=""
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");
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
expect(rustHost?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
2026-04-24 06:10:18 +08:00
expect(rustHost).not.toBeNull();
expect(iframe).not.toBeNull();
expect(iframe?.getAttribute("srcdoc")).toContain('"runtimeArtifact"');
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_tree_shell_runtime_artifact_v1"');
2026-04-26 04:29:23 +08:00
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
});
it("file tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
await act(async () => {
root.render(
<SidebarTreeSurface
mode="filetree"
rendererFamily="rust_family"
workspaceId="ws_1"
treeShellEnabled={false}
rows={[]}
activeId=""
onRowClick={() => undefined}
onRowDoubleClick={() => undefined}
onRowContextMenu={() => undefined}
onToggleExpand={() => undefined}
onCreateChild={() => undefined}
/>,
);
});
const surface = container.querySelector('[data-testid="sidebar-file-tree-shell"]');
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
2026-04-26 04:29:23 +08:00
expect(container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]')).not.toBeNull();
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
});
it("file tree 在缺少 workspaceId 时应显示 Rust 宿主占位,而不是回退旧 React renderer", () => {
act(() => {
root.render(
<SidebarTreeSurface
mode="filetree"
rendererFamily="rust_family"
workspaceId={null}
treeShellEnabled
rows={[]}
activeId=""
onRowClick={() => undefined}
onRowDoubleClick={() => undefined}
onRowContextMenu={() => undefined}
onToggleExpand={() => undefined}
onCreateChild={() => undefined}
/>,
);
});
const surface = container.querySelector('[data-testid="sidebar-file-tree-shell"]');
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("filetree_renderer_removed");
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).not.toBeNull();
2026-04-24 06:10:18 +08:00
});
it("file tree surface 应把 iframe 内部拖放与外部文件拖放桥接回宿主回调", async () => {
const onInternalDrop = vi.fn();
const onDropFiles = vi.fn();
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
2026-04-26 04:29:23 +08:00
rows={[]}
2026-04-24 06:10:18 +08:00
activeId=""
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,
2026-04-26 04:29:23 +08:00
rowId: "doc:doc_target",
rowKind: "doc",
documentId: "doc_target",
2026-04-24 06:10:18 +08:00
},
source: window,
}),
);
window.dispatchEvent(
new MessageEvent("message", {
data: {
channel: "sidebar-file-tree-shell",
type: "tree.filetree.external-drop",
documentId: "doc_target",
2026-04-26 04:29:23 +08:00
rowId: "doc:doc_target",
rowKind: "doc",
2026-04-24 06:10:18 +08:00
files: [droppedFile],
},
source: window,
}),
);
});
expect(onInternalDrop).toHaveBeenCalledWith({
2026-04-26 04:29:23 +08:00
targetDocumentId: "doc_target",
targetRowId: "doc:doc_target",
targetRowKind: "doc",
targetAssetId: null,
2026-04-24 06:10:18 +08:00
rowIds: ["doc:doc_source"],
copy: false,
});
2026-04-26 04:29:23 +08:00
expect(onDropFiles).toHaveBeenCalledWith({
targetDocumentId: "doc_target",
targetRowId: "doc:doc_target",
targetRowKind: "doc",
targetAssetId: null,
files: [droppedFile],
});
2026-04-24 06:10:18 +08:00
});
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");
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
expect(surface?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
expect(rustHost?.getAttribute("data-tree-renderer-contract")).toBe("rust_renderer_input_v1");
2026-04-24 06:10:18 +08:00
expect(rustHost).not.toBeNull();
expect(iframe).not.toBeNull();
expect(onPick).not.toHaveBeenCalled();
});
2026-04-26 04:29:23 +08:00
it("picker 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
await act(async () => {
2026-04-24 06:10:18 +08:00
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()}
/>,
);
});
2026-04-26 04:29:23 +08:00
await act(async () => {
await Promise.resolve();
});
2026-04-24 06:10:18 +08:00
const surface = container.querySelector('[data-testid="tree-picker-surface"]');
const rustHost = container.querySelector('[data-testid="tree-picker-surface-rust-host"]');
2026-04-26 04:29:23 +08:00
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
2026-04-24 06:10:18 +08:00
2026-04-26 19:35:52 +08:00
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
2026-04-24 06:10:18 +08:00
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
expect(rustHost).not.toBeNull();
2026-04-26 04:29:23 +08:00
expect(iframe).not.toBeNull();
2026-04-24 06:10:18 +08:00
});
});