4-10 树域 Rust 家族化
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user