267 lines
8.8 KiB
TypeScript
267 lines
8.8 KiB
TypeScript
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();
|
||
|
|
});
|
||
|
|
});
|