feat(tree): close rust family shell cutover
This commit is contained in:
@@ -1,10 +1,97 @@
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Mock } from "vitest";
|
||||
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
||||
import { SidebarTreeSurface, TreePickerSurface, type TreeRendererFamily } from "./tree-shell-surface";
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
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 ?? {};
|
||||
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,
|
||||
}]
|
||||
: [];
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
mode: "fileTree",
|
||||
state: { mode: "fileTree", state },
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe("tree-shell-surface", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
@@ -20,6 +107,7 @@ describe("tree-shell-surface", () => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
delete (globalThis as TreeShellRuntimeTestGlobal).__MNOTE_TREE_SHELL_RUNTIME__;
|
||||
});
|
||||
|
||||
function renderPageSurface(rendererFamily: TreeRendererFamily, focusedDocumentId?: string) {
|
||||
@@ -43,41 +131,41 @@ describe("tree-shell-surface", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("page tree surface 在 rust_family 下可切到同源 iframe host", () => {
|
||||
it("page tree surface 在 rust_family 下默认切到 Rust/WASM DOM shell 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"]');
|
||||
const domHost = container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');
|
||||
|
||||
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("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_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");
|
||||
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"');
|
||||
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();
|
||||
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("page tree surface 在 rust_family 下应把 focusedDocumentId 作为宿主状态暴露,并使用 postMessage patch 同步 iframe", () => {
|
||||
it("page tree surface 在 rust_family 下应把 focusedDocumentId 暴露到宿主 DOM 状态", () => {
|
||||
renderPageSurface("rust_family", "doc_focus");
|
||||
|
||||
const iframe = container.querySelector(
|
||||
'[data-testid="sidebar-page-tree-shell-rust-iframe"]',
|
||||
) as HTMLIFrameElement | null;
|
||||
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
const domHost = container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');
|
||||
|
||||
expect(surface?.getAttribute("data-page-tree-focused-id")).toBe("doc_focus");
|
||||
expect(iframe?.getAttribute("src")).toBeNull();
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"focusedDocumentId":"doc_focus"');
|
||||
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
||||
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("page tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
|
||||
it("page tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用默认 DOM shell host", async () => {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<SidebarTreeSurface
|
||||
@@ -97,8 +185,9 @@ describe("tree-shell-surface", () => {
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]')).not.toBeNull();
|
||||
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();
|
||||
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -126,7 +215,7 @@ describe("tree-shell-surface", () => {
|
||||
expect(container.querySelector('[data-testid="page-tree-renderer-removed"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("file tree surface 在 rust_family 下也应走同一 host 选择契约", () => {
|
||||
it("file tree surface 在 rust_family 下也应走默认 Rust/WASM DOM shell host", () => {
|
||||
act(() => {
|
||||
root.render(
|
||||
<SidebarTreeSurface
|
||||
@@ -149,21 +238,23 @@ describe("tree-shell-surface", () => {
|
||||
const rustHost = container.querySelector(
|
||||
'[data-testid="sidebar-file-tree-shell-rust-host"]',
|
||||
);
|
||||
const iframe = container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
|
||||
const domHost = container.querySelector('[data-testid="sidebar-file-tree-shell-dom-host"]');
|
||||
|
||||
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("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_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");
|
||||
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"');
|
||||
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();
|
||||
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("file tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
|
||||
it("file tree 在 rust_family 下即使 tree shellEnabled=false 也应继续使用默认 DOM shell host", async () => {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<SidebarTreeSurface
|
||||
@@ -183,8 +274,9 @@ describe("tree-shell-surface", () => {
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="sidebar-file-tree-shell"]');
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(container.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]')).not.toBeNull();
|
||||
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();
|
||||
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -212,10 +304,50 @@ describe("tree-shell-surface", () => {
|
||||
expect(container.querySelector('[data-testid="file-tree-renderer-removed"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("file tree surface 应把 iframe 内部拖放与外部文件拖放桥接回宿主回调", async () => {
|
||||
it("file tree surface 应把 DOM shell 内部拖放与外部文件拖放桥接回宿主回调", async () => {
|
||||
const runtimeReducerMock = mockTreeRuntimeReducer();
|
||||
const onInternalDrop = vi.fn();
|
||||
const onDropFiles = vi.fn();
|
||||
const droppedFile = new File(["bridge"], "bridge.txt", { type: "text/plain" });
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
@@ -225,6 +357,7 @@ describe("tree-shell-surface", () => {
|
||||
workspaceId="ws_1"
|
||||
treeShellEnabled
|
||||
rows={[]}
|
||||
treeShellItems={[targetProjectionItem]}
|
||||
activeId=""
|
||||
onRowClick={() => undefined}
|
||||
onRowDoubleClick={() => undefined}
|
||||
@@ -237,45 +370,33 @@ describe("tree-shell-surface", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const iframe = container.querySelector(
|
||||
'[data-testid="sidebar-file-tree-shell-rust-iframe"]',
|
||||
const targetRow = container.querySelector(
|
||||
'[data-testid="filetree-doc-row"][data-document-id="doc_target"]',
|
||||
);
|
||||
expect(iframe).not.toBeNull();
|
||||
Object.defineProperty(iframe, "contentWindow", {
|
||||
configurable: true,
|
||||
value: window,
|
||||
});
|
||||
expect(targetRow).not.toBeNull();
|
||||
|
||||
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,
|
||||
rowId: "doc:doc_target",
|
||||
rowKind: "doc",
|
||||
documentId: "doc_target",
|
||||
},
|
||||
source: window,
|
||||
}),
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
channel: "sidebar-file-tree-shell",
|
||||
type: "tree.filetree.external-drop",
|
||||
documentId: "doc_target",
|
||||
rowId: "doc:doc_target",
|
||||
rowKind: "doc",
|
||||
files: [droppedFile],
|
||||
},
|
||||
source: window,
|
||||
}),
|
||||
);
|
||||
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);
|
||||
});
|
||||
await waitForRuntimeReducer(runtimeReducerMock);
|
||||
|
||||
expect(runtimeReducerMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: "fileTree",
|
||||
action: expect.objectContaining({ kind: "dispatchInternalDrop" }),
|
||||
}),
|
||||
);
|
||||
expect(onInternalDrop).toHaveBeenCalledWith({
|
||||
targetDocumentId: "doc_target",
|
||||
targetRowId: "doc:doc_target",
|
||||
@@ -293,7 +414,7 @@ describe("tree-shell-surface", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("picker surface 在 rust_family 下也应挂到同一 host 边界", async () => {
|
||||
it("picker surface 在 rust_family 下也应挂到默认 Rust/WASM DOM shell host", async () => {
|
||||
const onPick = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
@@ -312,19 +433,24 @@ describe("tree-shell-surface", () => {
|
||||
|
||||
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"]');
|
||||
const domHost = container.querySelector('[data-testid="tree-picker-surface-dom-host"]');
|
||||
|
||||
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("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_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");
|
||||
expect(rustHost).not.toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
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();
|
||||
expect(onPick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("picker 在 rust_family 下即使 tree shellEnabled=false 也应继续使用 iframe host", async () => {
|
||||
it("picker 在 rust_family 下即使 tree shellEnabled=false 也应继续使用默认 DOM shell host", async () => {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<TreePickerSurface
|
||||
@@ -346,11 +472,12 @@ describe("tree-shell-surface", () => {
|
||||
|
||||
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"]');
|
||||
const domHost = container.querySelector('[data-testid="tree-picker-surface-dom-host"]');
|
||||
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(rustHost).not.toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(domHost).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user