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";
|
2026-04-28 16:30:51 +08:00
|
|
|
import type { Mock } from "vitest";
|
|
|
|
|
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
2026-04-24 06:10:18 +08:00
|
|
|
import { SidebarTreeSurface, TreePickerSurface, type TreeRendererFamily } from "./tree-shell-surface";
|
|
|
|
|
|
|
|
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
type RuntimeRequest = {
|
|
|
|
|
requestId: string;
|
|
|
|
|
mode: "page" | "fileTree" | "picker";
|
|
|
|
|
environment?: Record<string, unknown>;
|
|
|
|
|
state?: Record<string, unknown>;
|
|
|
|
|
action?: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type TreeShellRuntimeTestGlobal = typeof globalThis & {
|
|
|
|
|
__MNOTE_TREE_SHELL_RUNTIME__?: {
|
|
|
|
|
reduceTreeShellRuntime: Mock;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function reduceTreeRuntimeForTest(request: RuntimeRequest) {
|
|
|
|
|
if (request.mode === "fileTree") {
|
|
|
|
|
const state = request.state ?? {};
|
|
|
|
|
const action = request.action ?? {};
|
|
|
|
|
const env = request.environment ?? {};
|
2026-05-08 00:41:03 +08:00
|
|
|
const selection = (state.selection ?? {}) as Record<string, unknown>;
|
2026-04-28 16:30:51 +08:00
|
|
|
const rows = Array.isArray(env.rows) ? env.rows as Array<Record<string, unknown>> : [];
|
|
|
|
|
const rowId = typeof action.targetRowId === "string" ? action.targetRowId : null;
|
|
|
|
|
const targetRow = rows.find((row) => row.rowId === rowId) ?? {};
|
|
|
|
|
const target = {
|
|
|
|
|
kind: targetRow.rowKind ?? "doc",
|
|
|
|
|
documentId: targetRow.documentId ?? null,
|
|
|
|
|
assetId: targetRow.assetId ?? null,
|
|
|
|
|
};
|
|
|
|
|
const hostEvents =
|
|
|
|
|
action.kind === "dispatchInternalDrop"
|
|
|
|
|
? [{
|
|
|
|
|
kind: "fileTreeInternalDrop",
|
|
|
|
|
targetRowId: rowId,
|
|
|
|
|
target,
|
|
|
|
|
rowIds: Array.isArray(action.rowIds) ? action.rowIds : [],
|
|
|
|
|
copy: action.copy === true,
|
|
|
|
|
}]
|
|
|
|
|
: action.kind === "dispatchExternalDrop"
|
|
|
|
|
? [{
|
|
|
|
|
kind: "fileTreeExternalDrop",
|
|
|
|
|
targetRowId: rowId,
|
|
|
|
|
target,
|
|
|
|
|
fileCount: action.fileCount ?? 0,
|
2026-05-08 00:41:03 +08:00
|
|
|
}]
|
|
|
|
|
: [];
|
|
|
|
|
const selectedRowId = typeof action.rowId === "string" ? action.rowId : null;
|
|
|
|
|
const nextSelection =
|
|
|
|
|
action.kind === "selectRow" && selectedRowId
|
|
|
|
|
? { selectedRowIds: [selectedRowId], anchorRowId: selectedRowId, focusedRowId: selectedRowId }
|
|
|
|
|
: action.kind === "clearSelection"
|
|
|
|
|
? { selectedRowIds: [], anchorRowId: null, focusedRowId: null }
|
|
|
|
|
: selection;
|
2026-04-28 16:30:51 +08:00
|
|
|
return {
|
|
|
|
|
requestId: request.requestId,
|
|
|
|
|
mode: "fileTree",
|
2026-05-08 00:41:03 +08:00
|
|
|
state: {
|
|
|
|
|
mode: "fileTree",
|
|
|
|
|
state: {
|
|
|
|
|
activeRowId: (state as Record<string, unknown>).activeRowId ?? null,
|
|
|
|
|
selection: nextSelection,
|
|
|
|
|
dragRowIds: (state as Record<string, unknown>).dragRowIds ?? [],
|
|
|
|
|
dragEffect: (state as Record<string, unknown>).dragEffect ?? null,
|
|
|
|
|
dropTargetRowId: (state as Record<string, unknown>).dropTargetRowId ?? null,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-04-28 16:30:51 +08:00
|
|
|
domPatches: [{ kind: "fileTreeState" }],
|
|
|
|
|
hostEvents,
|
|
|
|
|
commandEvents: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
requestId: request.requestId,
|
|
|
|
|
mode: request.mode,
|
|
|
|
|
state: { mode: request.mode, state: request.state ?? {} },
|
|
|
|
|
domPatches: [],
|
|
|
|
|
hostEvents: [],
|
|
|
|
|
commandEvents: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockTreeRuntimeReducer() {
|
|
|
|
|
const reduceTreeShellRuntime = vi.fn(async (request: RuntimeRequest) =>
|
|
|
|
|
reduceTreeRuntimeForTest(request),
|
|
|
|
|
);
|
|
|
|
|
(globalThis as TreeShellRuntimeTestGlobal).__MNOTE_TREE_SHELL_RUNTIME__ = {
|
|
|
|
|
reduceTreeShellRuntime,
|
|
|
|
|
};
|
|
|
|
|
return reduceTreeShellRuntime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function waitForRuntimeReducer(reducerMock: Mock) {
|
|
|
|
|
for (let index = 0; index < 10; index += 1) {
|
|
|
|
|
if (reducerMock.mock.calls.length > 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await Promise.resolve();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 06:10:18 +08:00
|
|
|
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-28 16:30:51 +08:00
|
|
|
delete (globalThis as TreeShellRuntimeTestGlobal).__MNOTE_TREE_SHELL_RUNTIME__;
|
2026-04-24 06:10:18 +08:00
|
|
|
});
|
|
|
|
|
|
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}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("page tree surface 在 rust_family 下默认切到 Rust/WASM DOM shell host", () => {
|
2026-04-24 06:10:18 +08:00
|
|
|
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"]',
|
|
|
|
|
);
|
2026-04-28 16:30:51 +08:00
|
|
|
const domHost = container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');
|
2026-04-24 06:10:18 +08:00
|
|
|
|
|
|
|
|
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
|
|
|
|
|
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
2026-04-26 19:35:52 +08:00
|
|
|
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();
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(domHost).not.toBeNull();
|
|
|
|
|
expect(domHost?.getAttribute("data-tree-runtime-artifact-host")).toBe("rust_tree_shell_runtime_artifact_v1");
|
|
|
|
|
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-tree-browser-bridge="iframe_srcdoc"]')).toBeNull();
|
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-28 16:30:51 +08:00
|
|
|
it("page tree surface 在 rust_family 下应把 focusedDocumentId 暴露到宿主 DOM 状态", () => {
|
2026-04-26 04:29:23 +08:00
|
|
|
renderPageSurface("rust_family", "doc_focus");
|
|
|
|
|
|
2026-04-26 19:35:52 +08:00
|
|
|
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
2026-04-28 16:30:51 +08:00
|
|
|
const domHost = container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');
|
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");
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).toBeNull();
|
2026-04-26 04:29:23 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("page tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用默认 DOM shell host", async () => {
|
2026-04-26 04:29:23 +08:00
|
|
|
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-28 16:30:51 +08:00
|
|
|
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]')).not.toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).toBeNull();
|
2026-04-26 04:29:23 +08:00
|
|
|
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
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("file tree surface 在 rust_family 下也应走默认 Rust/WASM DOM shell host", () => {
|
2026-04-24 06:10:18 +08:00
|
|
|
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"]',
|
|
|
|
|
);
|
2026-04-28 16:30:51 +08:00
|
|
|
const domHost = container.querySelector('[data-testid="sidebar-file-tree-shell-dom-host"]');
|
2026-04-24 06:10:18 +08:00
|
|
|
|
|
|
|
|
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
|
|
|
|
|
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
2026-04-26 19:35:52 +08:00
|
|
|
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();
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(domHost).not.toBeNull();
|
|
|
|
|
expect(domHost?.getAttribute("data-tree-runtime-artifact-host")).toBe("rust_tree_shell_runtime_artifact_v1");
|
|
|
|
|
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]')).toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-tree-browser-bridge="iframe_srcdoc"]')).toBeNull();
|
2026-04-26 04:29:23 +08:00
|
|
|
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("file tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用默认 DOM shell host", async () => {
|
2026-04-26 04:29:23 +08:00
|
|
|
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-28 16:30:51 +08:00
|
|
|
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-file-tree-shell-dom-host"]')).not.toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]')).toBeNull();
|
2026-04-26 04:29:23 +08:00
|
|
|
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
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("file tree surface 应把 DOM shell 内部拖放与外部文件拖放桥接回宿主回调", async () => {
|
|
|
|
|
const runtimeReducerMock = mockTreeRuntimeReducer();
|
2026-04-24 06:10:18 +08:00
|
|
|
const onInternalDrop = vi.fn();
|
|
|
|
|
const onDropFiles = vi.fn();
|
|
|
|
|
const droppedFile = new File(["bridge"], "bridge.txt", { type: "text/plain" });
|
2026-04-28 16:30:51 +08:00
|
|
|
const dataTransfer = {
|
|
|
|
|
files: [] as File[],
|
|
|
|
|
types: ["application/x-mnote-file-tree", "text/plain"],
|
|
|
|
|
dropEffect: "move",
|
|
|
|
|
getData: (type: string) =>
|
|
|
|
|
type === "application/x-mnote-file-tree" || type === "text/plain"
|
|
|
|
|
? JSON.stringify({ type: "mnote-file-tree-dnd", version: 1, rowIds: ["doc:doc_source"] })
|
|
|
|
|
: "",
|
|
|
|
|
setData: vi.fn(),
|
|
|
|
|
} as unknown as DataTransfer;
|
|
|
|
|
const externalDataTransfer = {
|
|
|
|
|
files: [droppedFile],
|
|
|
|
|
types: ["Files"],
|
|
|
|
|
dropEffect: "copy",
|
|
|
|
|
getData: () => "",
|
|
|
|
|
setData: vi.fn(),
|
|
|
|
|
} as unknown as DataTransfer;
|
|
|
|
|
const targetProjectionItem: KernelFileTreeProjectionItem = {
|
|
|
|
|
rowId: "doc:doc_target",
|
|
|
|
|
nodeId: "doc_target",
|
|
|
|
|
parentNodeId: null,
|
|
|
|
|
projectionKind: "file_tree",
|
|
|
|
|
title: "目标页面",
|
|
|
|
|
depth: 0,
|
|
|
|
|
position: 0,
|
|
|
|
|
childCount: 0,
|
|
|
|
|
expandable: false,
|
|
|
|
|
expandedByDefault: false,
|
|
|
|
|
nodeType: "page",
|
|
|
|
|
rowKind: "document",
|
|
|
|
|
iconHint: "page",
|
|
|
|
|
capabilities: ["open", "select", "context-menu"],
|
|
|
|
|
resourceMeta: {
|
|
|
|
|
resourceKind: "document",
|
|
|
|
|
documentId: "doc_target",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
iconHint: "page",
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-04-24 06:10:18 +08:00
|
|
|
|
|
|
|
|
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-28 16:30:51 +08:00
|
|
|
treeShellItems={[targetProjectionItem]}
|
2026-04-24 06:10:18 +08:00
|
|
|
activeId=""
|
|
|
|
|
onRowClick={() => undefined}
|
|
|
|
|
onRowDoubleClick={() => undefined}
|
|
|
|
|
onRowContextMenu={() => undefined}
|
|
|
|
|
onToggleExpand={() => undefined}
|
|
|
|
|
onCreateChild={() => undefined}
|
|
|
|
|
onInternalDrop={onInternalDrop}
|
|
|
|
|
onDropFiles={onDropFiles}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
const targetRow = container.querySelector(
|
|
|
|
|
'[data-testid="filetree-doc-row"][data-document-id="doc_target"]',
|
2026-04-24 06:10:18 +08:00
|
|
|
);
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(targetRow).not.toBeNull();
|
2026-04-24 06:10:18 +08:00
|
|
|
|
|
|
|
|
await act(async () => {
|
2026-04-28 16:30:51 +08:00
|
|
|
const internalDropEvent = new Event("drop", { bubbles: true, cancelable: true });
|
|
|
|
|
Object.defineProperty(internalDropEvent, "dataTransfer", {
|
|
|
|
|
configurable: true,
|
|
|
|
|
value: dataTransfer,
|
|
|
|
|
});
|
|
|
|
|
targetRow?.dispatchEvent(internalDropEvent);
|
|
|
|
|
const externalDropEvent = new Event("drop", { bubbles: true, cancelable: true });
|
|
|
|
|
Object.defineProperty(externalDropEvent, "dataTransfer", {
|
|
|
|
|
configurable: true,
|
|
|
|
|
value: externalDataTransfer,
|
|
|
|
|
});
|
|
|
|
|
targetRow?.dispatchEvent(externalDropEvent);
|
2026-04-24 06:10:18 +08:00
|
|
|
});
|
2026-04-28 16:30:51 +08:00
|
|
|
await waitForRuntimeReducer(runtimeReducerMock);
|
2026-04-24 06:10:18 +08:00
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(runtimeReducerMock).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
mode: "fileTree",
|
|
|
|
|
action: expect.objectContaining({ kind: "dispatchInternalDrop" }),
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-04-24 06:10:18 +08:00
|
|
|
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
|
|
|
});
|
|
|
|
|
|
2026-05-08 00:41:03 +08:00
|
|
|
it("file tree surface 应支持空白区清空选择", async () => {
|
|
|
|
|
const runtimeReducerMock = mockTreeRuntimeReducer();
|
|
|
|
|
const targetProjectionItem: KernelFileTreeProjectionItem = {
|
|
|
|
|
rowId: "doc:doc_target",
|
|
|
|
|
nodeId: "doc_target",
|
|
|
|
|
parentNodeId: null,
|
|
|
|
|
projectionKind: "file_tree",
|
|
|
|
|
title: "目标页面",
|
|
|
|
|
depth: 0,
|
|
|
|
|
position: 0,
|
|
|
|
|
childCount: 0,
|
|
|
|
|
expandable: false,
|
|
|
|
|
expandedByDefault: false,
|
|
|
|
|
nodeType: "page",
|
|
|
|
|
rowKind: "document",
|
|
|
|
|
iconHint: "page",
|
|
|
|
|
capabilities: ["open", "select", "context-menu"],
|
|
|
|
|
resourceMeta: {
|
|
|
|
|
resourceKind: "document",
|
|
|
|
|
documentId: "doc_target",
|
|
|
|
|
workspaceId: "ws_1",
|
|
|
|
|
iconHint: "page",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
root.render(
|
|
|
|
|
<SidebarTreeSurface
|
|
|
|
|
mode="filetree"
|
|
|
|
|
rendererFamily="rust_family"
|
|
|
|
|
workspaceId="ws_1"
|
|
|
|
|
treeShellEnabled
|
|
|
|
|
rows={[]}
|
|
|
|
|
treeShellItems={[targetProjectionItem]}
|
|
|
|
|
activeId=""
|
|
|
|
|
onRowClick={() => undefined}
|
|
|
|
|
onRowDoubleClick={() => undefined}
|
|
|
|
|
onRowContextMenu={() => undefined}
|
|
|
|
|
onToggleExpand={() => undefined}
|
|
|
|
|
onCreateChild={() => undefined}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const targetRow = container.querySelector('[data-testid="filetree-doc-row"][data-row-id="doc:doc_target"]') as HTMLElement | null;
|
|
|
|
|
const treeRoot = container.querySelector('[data-rust-filetree-renderer="dom_v1"]') as HTMLElement | null;
|
|
|
|
|
expect(targetRow).not.toBeNull();
|
|
|
|
|
expect(treeRoot).not.toBeNull();
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
targetRow?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
|
|
|
});
|
|
|
|
|
await waitForRuntimeReducer(runtimeReducerMock);
|
|
|
|
|
expect(targetRow?.getAttribute("data-selected")).toBe("true");
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
treeRoot?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
|
|
|
|
|
await Promise.resolve();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(targetRow?.getAttribute("data-selected")).toBe("false");
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("picker surface 在 rust_family 下也应挂到默认 Rust/WASM DOM shell host", async () => {
|
2026-04-24 06:10:18 +08:00
|
|
|
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"]');
|
2026-04-28 16:30:51 +08:00
|
|
|
const domHost = container.querySelector('[data-testid="tree-picker-surface-dom-host"]');
|
2026-04-24 06:10:18 +08:00
|
|
|
|
|
|
|
|
expect(surface?.getAttribute("data-renderer-family")).toBe("rust_family");
|
|
|
|
|
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
2026-04-26 19:35:52 +08:00
|
|
|
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();
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(domHost).not.toBeNull();
|
|
|
|
|
expect(domHost?.getAttribute("data-tree-runtime-artifact-host")).toBe("rust_tree_shell_runtime_artifact_v1");
|
|
|
|
|
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
|
|
|
|
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-tree-browser-bridge="iframe_srcdoc"]')).toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_1"]')).not.toBeNull();
|
2026-04-24 06:10:18 +08:00
|
|
|
expect(onPick).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
it("picker 在 rust_family 下即使 tree shellEnabled=false 也应继续使用默认 DOM shell host", async () => {
|
2026-04-26 04:29:23 +08:00
|
|
|
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-28 16:30:51 +08:00
|
|
|
const domHost = container.querySelector('[data-testid="tree-picker-surface-dom-host"]');
|
2026-04-24 06:10:18 +08:00
|
|
|
|
2026-04-28 16:30:51 +08:00
|
|
|
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_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-28 16:30:51 +08:00
|
|
|
expect(domHost).not.toBeNull();
|
|
|
|
|
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
2026-04-24 06:10:18 +08:00
|
|
|
});
|
|
|
|
|
});
|