feat(tree): close rust family shell cutover
This commit is contained in:
@@ -2,10 +2,14 @@ import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import React, { type ReactNode } from "react";
|
||||
import type { Mock } from "vitest";
|
||||
import type { SidebarTreeNode } from "@/lib/kernel-sidebar";
|
||||
import { MoveEmbedPickerDialog } from "./move-embed-picker-dialog";
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
type SidebarFixtureNode = SidebarTreeNode;
|
||||
|
||||
const sidebarData = {
|
||||
activeWorkspaceId: "ws_test",
|
||||
workspaces: [],
|
||||
@@ -17,15 +21,15 @@ const sidebarData = {
|
||||
items: [],
|
||||
edges: [],
|
||||
},
|
||||
kernelSidebarTree: [],
|
||||
kernelSidebarTree: [] as SidebarFixtureNode[],
|
||||
trashedDocuments: [],
|
||||
};
|
||||
|
||||
function buildSidebarNode(input: {
|
||||
id: string;
|
||||
title: string;
|
||||
children?: Array<ReturnType<typeof buildSidebarNode>>;
|
||||
}) {
|
||||
children?: SidebarFixtureNode[];
|
||||
}): SidebarFixtureNode {
|
||||
return {
|
||||
access_scope: "private",
|
||||
id: input.id,
|
||||
@@ -66,12 +70,89 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseDocumentSearch = vi.fn(() => ({
|
||||
const mockUseDocumentSearch: Mock = vi.fn(() => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
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 reducePickerRuntimeForTest(request: RuntimeRequest) {
|
||||
const action = request.action ?? {};
|
||||
const env = request.environment ?? {};
|
||||
const items = Array.isArray(env.items) ? env.items as Array<Record<string, unknown>> : [];
|
||||
const state = request.state ?? {};
|
||||
const pickable = items.filter((item) => item.pickable !== false);
|
||||
const activeItemKey = typeof state.activeItemKey === "string" ? state.activeItemKey : null;
|
||||
const activeIndex = Math.max(0, pickable.findIndex((item) => item.itemKey === activeItemKey));
|
||||
let nextItemKey = activeItemKey;
|
||||
|
||||
if (action.kind === "focus") {
|
||||
nextItemKey = typeof action.itemKey === "string" ? action.itemKey : null;
|
||||
} else if (action.kind === "next") {
|
||||
nextItemKey = String(pickable[Math.min(pickable.length - 1, activeIndex + 1)]?.itemKey ?? "");
|
||||
} else if (action.kind === "previous") {
|
||||
nextItemKey = String(pickable[Math.max(0, activeIndex - 1)]?.itemKey ?? "");
|
||||
} else if (action.kind === "home") {
|
||||
nextItemKey = String(pickable[0]?.itemKey ?? "");
|
||||
} else if (action.kind === "end") {
|
||||
nextItemKey = String(pickable[pickable.length - 1]?.itemKey ?? "");
|
||||
}
|
||||
|
||||
const nextState = { activeItemKey: nextItemKey || null };
|
||||
const hostEvents =
|
||||
action.kind === "pick"
|
||||
? activeItemKey === "__root__"
|
||||
? [{ kind: "pickerPickRoot" }]
|
||||
: activeItemKey
|
||||
? [{ kind: "pickerPickDocument", documentId: activeItemKey }]
|
||||
: []
|
||||
: [];
|
||||
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
mode: "picker",
|
||||
state: { mode: "picker", state: nextState },
|
||||
domPatches: [{ kind: "pickerState", activeItemKey: nextState.activeItemKey, focusDom: false }],
|
||||
hostEvents,
|
||||
commandEvents: [],
|
||||
};
|
||||
}
|
||||
|
||||
function mockTreeRuntimeReducer() {
|
||||
const reduceTreeShellRuntime = vi.fn(async (request: RuntimeRequest) =>
|
||||
reducePickerRuntimeForTest(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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("@/hooks/use-document-search", () => ({
|
||||
useDocumentSearch: (...args: unknown[]) => mockUseDocumentSearch(...args),
|
||||
}));
|
||||
@@ -142,6 +223,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
error: null,
|
||||
});
|
||||
sidebarData.kernelSidebarTree = [];
|
||||
delete (globalThis as TreeShellRuntimeTestGlobal).__MNOTE_TREE_SHELL_RUNTIME__;
|
||||
delete window.__MNOTE_RUNTIME_CONFIG__;
|
||||
});
|
||||
|
||||
@@ -438,6 +520,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
window.__MNOTE_RUNTIME_CONFIG__ = {
|
||||
treeRendererFamily: "rust_family",
|
||||
};
|
||||
mockTreeRuntimeReducer();
|
||||
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
@@ -458,13 +541,15 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
|
||||
const emptySurface = container.querySelector('[data-testid="tree-picker-surface"]');
|
||||
expect(emptySurface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(emptySurface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(emptySurface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-host"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
container.querySelector('[data-testid="tree-picker-surface-dom-host"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
||||
expect(container.querySelector('[data-tree-browser-bridge="iframe_srcdoc"]')).toBeNull();
|
||||
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
data: {
|
||||
@@ -494,19 +579,22 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
|
||||
const resultSurface = container.querySelector('[data-testid="tree-picker-surface"]');
|
||||
expect(resultSurface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(resultSurface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(resultSurface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-host"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
container.querySelector('[data-testid="tree-picker-surface-dom-host"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_target"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("rust_family 配置下,输入框键盘命令应转发给 iframe,并用焦点回传更新 shell 状态", async () => {
|
||||
it("rust_family 配置下,输入框键盘命令应驱动 DOM shell 高亮并选中当前项", async () => {
|
||||
window.__MNOTE_RUNTIME_CONFIG__ = {
|
||||
treeRendererFamily: "rust_family",
|
||||
};
|
||||
const runtimeReducerMock = mockTreeRuntimeReducer();
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
data: {
|
||||
results: [
|
||||
@@ -525,23 +613,19 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
'<!doctype html><html><body><script id="tree-shell-state" type="application/json">{}</script></body></html>',
|
||||
{ status: 200, headers: { "content-type": "text/html" } },
|
||||
),
|
||||
);
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onOpenChange={onOpenChange}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot
|
||||
excludeIds={[]}
|
||||
onPick={vi.fn(async () => undefined)}
|
||||
onPick={onPick}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
@@ -558,80 +642,41 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
input?.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
|
||||
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
|
||||
expect(iframe).not.toBeNull();
|
||||
Object.defineProperty(iframe, "contentWindow", {
|
||||
configurable: true,
|
||||
value: window,
|
||||
});
|
||||
const postMessageMock = vi.spyOn(window, "postMessage").mockImplementation(() => undefined);
|
||||
postMessageMock.mockClear();
|
||||
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-surface-dom-host"]')).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_first"]')?.getAttribute("data-focused"),
|
||||
).toBe("true");
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
await waitForRuntimeReducer(runtimeReducerMock);
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect(runtimeReducerMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "tree-picker-surface",
|
||||
type: "tree.picker.command",
|
||||
command: "next",
|
||||
mode: "picker",
|
||||
action: expect.objectContaining({ kind: "next" }),
|
||||
}),
|
||||
"*",
|
||||
);
|
||||
|
||||
postMessageMock.mockClear();
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
channel: "tree-picker-surface",
|
||||
type: "tree.picker.focus.changed",
|
||||
documentId: "doc_second",
|
||||
itemKey: "doc_second",
|
||||
},
|
||||
source: window,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "tree-picker-surface",
|
||||
type: "tree.shell.state.patch",
|
||||
activeDocumentId: "doc_second",
|
||||
activePickerItemKey: "doc_second",
|
||||
}),
|
||||
"*",
|
||||
);
|
||||
|
||||
postMessageMock.mockClear();
|
||||
expect(
|
||||
container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_second"]')?.getAttribute("data-focused"),
|
||||
).toBe("true");
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "tree-picker-surface",
|
||||
type: "tree.picker.command",
|
||||
command: "pick",
|
||||
}),
|
||||
"*",
|
||||
);
|
||||
expect(onPick).toHaveBeenCalledWith("move", "doc_second");
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("rust_family 配置下,无根目录且无结果时也应保持 same-origin host 空态", async () => {
|
||||
it("rust_family 配置下,无根目录且无结果时也应保持 DOM shell 空态", async () => {
|
||||
window.__MNOTE_RUNTIME_CONFIG__ = {
|
||||
treeRendererFamily: "rust_family",
|
||||
};
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(
|
||||
'<!doctype html><html><body><script id="tree-shell-state" type="application/json">{}</script></body></html>',
|
||||
{ status: 200, headers: { "content-type": "text/html" } },
|
||||
),
|
||||
);
|
||||
mockTreeRuntimeReducer();
|
||||
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
@@ -655,12 +700,14 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="tree-picker-surface"]');
|
||||
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-kind")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_inline_compat_host");
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
||||
expect(domHost).not.toBeNull();
|
||||
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
||||
expect(container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]')).toBeNull();
|
||||
expect(container.textContent).toContain("没有匹配结果");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
||||
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
|
||||
|
||||
export type TreeShellPickerItem =
|
||||
| { kind: "root"; id: null; title: string; subtitle?: string; depth?: number }
|
||||
| { kind: "doc"; id: string; title: string; subtitle?: string; depth?: number };
|
||||
|
||||
export type TreeShellDomProjectionItem = {
|
||||
rowId?: string;
|
||||
nodeId: string;
|
||||
parentNodeId: string | null;
|
||||
title: string;
|
||||
depth: number;
|
||||
childCount: number;
|
||||
position: number;
|
||||
expandedByDefault: boolean;
|
||||
rowKind?: string;
|
||||
iconHint?: string;
|
||||
capabilities?: string[];
|
||||
resourceMeta?: {
|
||||
resourceKind?: string;
|
||||
documentId?: string;
|
||||
assetId?: string;
|
||||
workspaceId?: string;
|
||||
assetKind?: string;
|
||||
iconHint?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeString = (value: unknown, fallback = "") => {
|
||||
if (typeof value !== "string") {
|
||||
return fallback;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || fallback;
|
||||
};
|
||||
|
||||
export function buildTreeShellDomPickerItems(
|
||||
items: TreeShellPickerItem[],
|
||||
): TreeShellDomProjectionItem[] {
|
||||
return items
|
||||
.filter(
|
||||
(item): item is Extract<TreeShellPickerItem, { kind: "doc"; id: string }> =>
|
||||
item.kind === "doc" && Boolean(normalizeString(item.id)),
|
||||
)
|
||||
.map((item, index) => ({
|
||||
nodeId: normalizeString(item.id),
|
||||
parentNodeId: null,
|
||||
title: normalizeString(item.title, "无标题"),
|
||||
depth: typeof item.depth === "number" && Number.isFinite(item.depth) ? Math.max(0, item.depth) : 0,
|
||||
childCount: 0,
|
||||
position: index,
|
||||
expandedByDefault: false,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildTreeShellDomPageItems(
|
||||
items: PageTreeProjectionItem[],
|
||||
expanded: ReadonlySet<string> = new Set(),
|
||||
): TreeShellDomProjectionItem[] {
|
||||
return items
|
||||
.map((item) => ({
|
||||
rowId: normalizeString(item.rowId),
|
||||
nodeId: normalizeString(item.nodeId),
|
||||
parentNodeId: item.parentNodeId ?? null,
|
||||
title: normalizeString(item.title, "无标题"),
|
||||
depth: typeof item.depth === "number" && Number.isFinite(item.depth) ? Math.max(0, item.depth) : 0,
|
||||
childCount:
|
||||
typeof item.childCount === "number" && Number.isFinite(item.childCount)
|
||||
? Math.max(0, item.childCount)
|
||||
: 0,
|
||||
position: typeof item.position === "number" && Number.isFinite(item.position) ? item.position : 0,
|
||||
expandedByDefault: item.childCount > 0 ? expanded.has(item.nodeId) : false,
|
||||
rowKind: "document",
|
||||
iconHint: normalizeString(item.iconHint, "page"),
|
||||
capabilities: Array.isArray(item.capabilities)
|
||||
? item.capabilities.map((capability) => normalizeString(capability)).filter(Boolean)
|
||||
: [],
|
||||
resourceMeta: {
|
||||
resourceKind: normalizeString(item.resourceMeta?.resourceKind, "document"),
|
||||
documentId: normalizeString(item.resourceMeta?.documentId ?? item.nodeId),
|
||||
workspaceId: normalizeString(item.resourceMeta?.workspaceId),
|
||||
iconHint: normalizeString(item.resourceMeta?.iconHint, "page"),
|
||||
},
|
||||
}))
|
||||
.filter((item) => Boolean(item.nodeId));
|
||||
}
|
||||
|
||||
export function buildTreeShellDomKernelFileTreeItems(
|
||||
items: KernelFileTreeProjectionItem[],
|
||||
): TreeShellDomProjectionItem[] {
|
||||
return items
|
||||
.map((item) => ({
|
||||
rowId: normalizeString(item.rowId),
|
||||
nodeId: normalizeString(item.nodeId),
|
||||
parentNodeId: item.parentNodeId ?? null,
|
||||
title: normalizeString(item.title, "无标题"),
|
||||
depth: typeof item.depth === "number" && Number.isFinite(item.depth) ? Math.max(0, item.depth) : 0,
|
||||
childCount:
|
||||
typeof item.childCount === "number" && Number.isFinite(item.childCount)
|
||||
? Math.max(0, item.childCount)
|
||||
: 0,
|
||||
position: typeof item.position === "number" && Number.isFinite(item.position) ? item.position : 0,
|
||||
expandedByDefault: item.expandedByDefault === true,
|
||||
rowKind: normalizeString(item.rowKind, "document"),
|
||||
iconHint: normalizeString(item.iconHint, "page"),
|
||||
capabilities: Array.isArray(item.capabilities)
|
||||
? item.capabilities.map((capability) => normalizeString(capability)).filter(Boolean)
|
||||
: [],
|
||||
resourceMeta: {
|
||||
resourceKind: normalizeString(item.resourceMeta?.resourceKind),
|
||||
documentId: normalizeString(item.resourceMeta?.documentId),
|
||||
assetId: normalizeString(item.resourceMeta?.assetId),
|
||||
workspaceId: normalizeString(item.resourceMeta?.workspaceId),
|
||||
assetKind: normalizeString(item.resourceMeta?.assetKind),
|
||||
iconHint: normalizeString(item.resourceMeta?.iconHint, normalizeString(item.iconHint, "page")),
|
||||
},
|
||||
}))
|
||||
.filter((item) => Boolean(item.nodeId));
|
||||
}
|
||||
|
||||
export function buildTreeShellDomChildrenByParent(items: TreeShellDomProjectionItem[]) {
|
||||
const ids = new Set(items.map((item) => item.nodeId));
|
||||
const childrenByParent = new Map<string, TreeShellDomProjectionItem[]>();
|
||||
for (const item of items) {
|
||||
const parentId = item.parentNodeId && ids.has(item.parentNodeId) ? item.parentNodeId : "";
|
||||
const bucket = childrenByParent.get(parentId) ?? [];
|
||||
bucket.push(item);
|
||||
childrenByParent.set(parentId, bucket);
|
||||
}
|
||||
for (const bucket of childrenByParent.values()) {
|
||||
bucket.sort((left, right) => {
|
||||
const byPosition = left.position - right.position;
|
||||
if (byPosition !== 0) return byPosition;
|
||||
return left.title.localeCompare(right.title, "zh-CN");
|
||||
});
|
||||
}
|
||||
return childrenByParent;
|
||||
}
|
||||
|
||||
export function buildTreeShellDomFiletreeSelection(activeDocumentId?: string | null) {
|
||||
const documentId = normalizeString(activeDocumentId);
|
||||
if (!documentId) {
|
||||
return {
|
||||
selectedRowIds: [] as string[],
|
||||
anchorRowId: null as string | null,
|
||||
focusedRowId: null as string | null,
|
||||
};
|
||||
}
|
||||
const documentRowId = `doc:${documentId}`;
|
||||
return {
|
||||
selectedRowIds: [documentRowId, `index:${documentId}`],
|
||||
anchorRowId: documentRowId,
|
||||
focusedRowId: documentRowId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { TreeShellHost } from "./tree-shell-host";
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe("tree-shell-host", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let previousLegacyFlag: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
previousLegacyFlag = process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
|
||||
delete process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
if (previousLegacyFlag === undefined) {
|
||||
delete process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST;
|
||||
} else {
|
||||
process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST = previousLegacyFlag;
|
||||
}
|
||||
});
|
||||
|
||||
function renderHost() {
|
||||
act(() => {
|
||||
root.render(
|
||||
<TreeShellHost
|
||||
mode="page"
|
||||
surfaceTestId="sidebar-page-tree-shell"
|
||||
rendererFamily="rust_family"
|
||||
workspaceId="ws_1"
|
||||
>
|
||||
<div data-testid="legacy-react-fallback">旧 React fallback</div>
|
||||
</TreeShellHost>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("rust_family 默认选择 Rust/WASM DOM shell host,不能进入 iframe_srcdoc", () => {
|
||||
renderHost();
|
||||
|
||||
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
const rustHost = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-host"]');
|
||||
const domHost = container.querySelector('[data-testid="sidebar-page-tree-shell-dom-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-host-implementation")).toBe("rust_wasm_dom_shell_host");
|
||||
expect(domHost?.getAttribute("data-tree-browser-bridge")).toBe("dom_wasm");
|
||||
expect(domHost?.getAttribute("data-tree-runtime-artifact-host")).toBe("rust_tree_shell_runtime_artifact_v1");
|
||||
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="legacy-react-fallback"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("只有显式 legacy flag 才允许进入旧 TreeShellIframeHost", async () => {
|
||||
process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST = "1";
|
||||
|
||||
await act(async () => {
|
||||
renderHost();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
const iframe = container.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
|
||||
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("rust_runtime_artifact_host");
|
||||
expect(container.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]')).toBeNull();
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(iframe?.getAttribute("data-tree-browser-bridge")).toBe("iframe_srcdoc");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"browserBridge":"iframe_srcdoc"');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { TreeShellRustDomShellHost } from "@/components/sidebar/tree-shell-dom-host";
|
||||
import type { TreeShellPickerItem } from "@/components/sidebar/tree-shell-dom-model";
|
||||
import {
|
||||
TreeShellIframeHost,
|
||||
type TreeShellPickerItem,
|
||||
} from "@/components/sidebar/tree-shell-iframe-host";
|
||||
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
||||
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
|
||||
@@ -14,6 +15,8 @@ export type TreeRendererFamily = "react" | "rust_family";
|
||||
export type TreeShellHostMode = "page" | "filetree" | "picker";
|
||||
|
||||
const RUST_RENDERER_CONTRACT = "rust_renderer_input_v1";
|
||||
const RUST_WASM_DOM_SHELL_HOST = "rust_wasm_dom_shell_host";
|
||||
const RUST_LEGACY_IFRAME_HOST = "rust_runtime_artifact_host";
|
||||
|
||||
export type TreeShellPickerCommand = {
|
||||
kind: "next" | "previous" | "home" | "end" | "pick";
|
||||
@@ -118,11 +121,17 @@ export function TreeShellHost({
|
||||
children,
|
||||
}: TreeShellHostProps) {
|
||||
const useRustHost = rendererFamily === "rust_family";
|
||||
const useIframeHost = useRustHost && Boolean(workspaceId?.trim());
|
||||
const useDomHost = useRustHost && Boolean(workspaceId?.trim());
|
||||
const useLegacyIframeHost =
|
||||
useDomHost &&
|
||||
typeof process !== "undefined" &&
|
||||
process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST === "1";
|
||||
const hostKind = rendererFamily === "rust_family" ? "rust_family" : "react";
|
||||
const rendererContract = useRustHost ? RUST_RENDERER_CONTRACT : undefined;
|
||||
const implementation = useIframeHost
|
||||
? "rust_inline_compat_host"
|
||||
const implementation = useLegacyIframeHost
|
||||
? RUST_LEGACY_IFRAME_HOST
|
||||
: useDomHost
|
||||
? RUST_WASM_DOM_SHELL_HOST
|
||||
: fallbackImplementation ??
|
||||
(rendererFamily === "rust_family" ? "react_fallback" : "react_primary");
|
||||
|
||||
@@ -146,36 +155,70 @@ export function TreeShellHost({
|
||||
data-tree-renderer-contract={rendererContract}
|
||||
className="contents"
|
||||
>
|
||||
{useIframeHost && workspaceId ? (
|
||||
<TreeShellIframeHost
|
||||
mode={mode}
|
||||
surfaceTestId={surfaceTestId}
|
||||
workspaceId={workspaceId}
|
||||
rootNodeId={rootNodeId}
|
||||
activeDocumentId={activeDocumentId}
|
||||
focusedDocumentId={focusedDocumentId}
|
||||
activePickerItemKey={activePickerItemKey}
|
||||
allowRootPick={allowRootPick}
|
||||
excludeIds={excludeIds}
|
||||
pickerCommand={pickerCommand}
|
||||
pickerItems={pickerItems}
|
||||
pageTreeItems={pageTreeItems}
|
||||
inlineFileTreeItems={inlineFileTreeItems}
|
||||
channel={channel}
|
||||
host={host}
|
||||
onNavigate={onNavigate}
|
||||
onPick={onPick}
|
||||
onPickerFocusChange={onPickerFocusChange}
|
||||
onPageContextMenu={onPageContextMenu}
|
||||
onPageExpandChange={onPageExpandChange}
|
||||
onPageFocusChange={onPageFocusChange}
|
||||
onFileTreeContextMenu={onFileTreeContextMenu}
|
||||
onFileTreeSelectionChange={onFileTreeSelectionChange}
|
||||
onInternalDrop={onInternalDrop}
|
||||
onDropFiles={onDropFiles}
|
||||
onAssetOpen={onAssetOpen}
|
||||
onTreeMutation={onTreeMutation}
|
||||
/>
|
||||
{useDomHost && workspaceId ? (
|
||||
useLegacyIframeHost ? (
|
||||
<TreeShellIframeHost
|
||||
mode={mode}
|
||||
surfaceTestId={surfaceTestId}
|
||||
workspaceId={workspaceId}
|
||||
rootNodeId={rootNodeId}
|
||||
activeDocumentId={activeDocumentId}
|
||||
focusedDocumentId={focusedDocumentId}
|
||||
activePickerItemKey={activePickerItemKey}
|
||||
allowRootPick={allowRootPick}
|
||||
excludeIds={excludeIds}
|
||||
pickerCommand={pickerCommand}
|
||||
pickerItems={pickerItems}
|
||||
pageTreeItems={pageTreeItems}
|
||||
inlineFileTreeItems={inlineFileTreeItems}
|
||||
channel={channel}
|
||||
host={host}
|
||||
onNavigate={onNavigate}
|
||||
onPick={onPick}
|
||||
onPickerFocusChange={onPickerFocusChange}
|
||||
onPageContextMenu={onPageContextMenu}
|
||||
onPageExpandChange={onPageExpandChange}
|
||||
onPageFocusChange={onPageFocusChange}
|
||||
onFileTreeContextMenu={onFileTreeContextMenu}
|
||||
onFileTreeSelectionChange={onFileTreeSelectionChange}
|
||||
onInternalDrop={onInternalDrop}
|
||||
onDropFiles={onDropFiles}
|
||||
onAssetOpen={onAssetOpen}
|
||||
onTreeMutation={onTreeMutation}
|
||||
/>
|
||||
) : (
|
||||
<TreeShellRustDomShellHost
|
||||
mode={mode}
|
||||
surfaceTestId={surfaceTestId}
|
||||
workspaceId={workspaceId}
|
||||
rootNodeId={rootNodeId}
|
||||
activeDocumentId={activeDocumentId}
|
||||
focusedDocumentId={focusedDocumentId}
|
||||
activePickerItemKey={activePickerItemKey}
|
||||
allowRootPick={allowRootPick}
|
||||
excludeIds={excludeIds}
|
||||
pickerCommand={pickerCommand}
|
||||
pickerItems={pickerItems}
|
||||
pageTreeItems={pageTreeItems}
|
||||
inlineFileTreeItems={inlineFileTreeItems}
|
||||
channel={channel}
|
||||
host={host}
|
||||
onNavigate={onNavigate}
|
||||
onPick={onPick}
|
||||
onPickerFocusChange={onPickerFocusChange}
|
||||
onPageContextMenu={onPageContextMenu}
|
||||
onPageExpandChange={onPageExpandChange}
|
||||
onPageFocusChange={onPageFocusChange}
|
||||
onFileTreeContextMenu={onFileTreeContextMenu}
|
||||
onFileTreeSelectionChange={onFileTreeSelectionChange}
|
||||
onInternalDrop={onInternalDrop}
|
||||
onDropFiles={onDropFiles}
|
||||
onAssetOpen={onAssetOpen}
|
||||
onTreeMutation={onTreeMutation}
|
||||
>
|
||||
{children}
|
||||
</TreeShellRustDomShellHost>
|
||||
)
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
@@ -286,11 +286,52 @@ describe("tree-shell-iframe-host", () => {
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("patchPageTreeExpansionDom(nodeId)");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_page_focus_keyboard_reducer_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_tree_shell_runtime_artifact_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"family":"rust_family"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"executionStrategy":"browser_bridge"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"browserBridge":"iframe_srcdoc"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"wasmModuleUrl":"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"jsGlueUrl":"/api/tree-shell-runtime/mnote-tree-shell-runtime.js"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"inputFields":["rendererInput","projectionItems","expandedIds","selectedRowIds","activePickerItem","focusedId"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"outputChannels":["domPatch","intentEvent","commandDispatchEvent"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"runtimeApi":{"requestContract":"TreeShellRuntimeRequest","resultContract":"TreeShellRuntimeResult"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"reduceEndpoint":"/api/tree/runtime/reduce"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"domPatchKinds":["pageState","fileTreeState","pickerState"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"hostEventKinds":["pageOpen","pageContextMenu","fileTreeOpen","fileTreeContextMenu","fileTreeInternalDrop","fileTreeExternalDrop","pickerPickRoot","pickerPickDocument"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"commandEventKinds":["createNode","renameNode","moveSubtree","copyResource","moveResource","uploadResource"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"eventKinds":["focus","keyboard","expandCollapse","selection","contextMenu","dragDrop","pick"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyPageKeyboardAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('if (mode === "page" && usedRustInitialRenderer) {patchPageTreeActiveDom();if (focusedNodeId) focusRowElement(focusedNodeId);return;}');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("loadTreeShellWasmRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceTreeShellRuntimeViaWasm");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reducePageActionWithRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceTreeShellRuntimeWithArtifact");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('mode: "page"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("buildPageRuntimeEnvironment");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rows: normalizedItems.map((entry) => ({nodeId: entry.nodeId,parentNodeId: normalizeText(entry.parentNodeId) || null,position: normalizeNumber(entry.position, 0),}))");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("moveNext");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("openFocused");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("contextMenuFocused");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("updateDropFeedback");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("updateDropFeedbackForTarget");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchMove");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchMoveToTarget");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("commandEvent.sortOrder");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("runtimeRequired: true");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("canAcceptPageDrop");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchCreate");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchRename");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("pageOpen");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("replayPageRuntimeHostEvents");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("replayPageRuntimeCommandEvents");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("commandEvents");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('event.kind === "createNode"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('event.kind === "renameNode"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dropFeedback");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("pagePatch?.dropFeedback");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("stateSnapshot?.dropFeedback");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("data-drop-feedback");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('event.kind === "moveSubtree"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('postToHost("tree.subtree.moved"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('if (mode === "page" && usedRustInitialRenderer) {patchPageTreeActiveDom();syncPageDropFeedbackDom();if (focusedNodeId) focusRowElement(focusedNodeId);return;}');
|
||||
expect(readTreeShellState(iframe?.getAttribute("srcdoc")).items).toEqual([
|
||||
expect.objectContaining({ nodeId: "doc_parent" }),
|
||||
expect.objectContaining({ nodeId: "doc_child", parentNodeId: "doc_parent" }),
|
||||
@@ -465,7 +506,7 @@ describe("tree-shell-iframe-host", () => {
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("hydrateInitialFileTree");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const usedRustInitialRenderer = hydrateInitialRenderer();");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("patchFileTreeActiveDom();");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('if (mode === "filetree" && usedRustInitialRenderer) {patchFileTreeActiveDom();syncFileTreeSelectionDom();return;}');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('if (mode === "filetree" && usedRustInitialRenderer) {patchFileTreeActiveDom();syncFileTreeSelectionDom();syncFileTreeDropTargetDom();return;}');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"rendererInput"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"mode":"fileTree"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"expandedIds":["doc_a"]');
|
||||
@@ -474,11 +515,45 @@ describe("tree-shell-iframe-host", () => {
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"selectedRowIds":["doc:doc_a","index:doc_a"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_filetree_selection_reducer_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_tree_shell_runtime_artifact_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"family":"rust_family"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"executionStrategy":"browser_bridge"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"browserBridge":"iframe_srcdoc"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"wasmModuleUrl":"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"jsGlueUrl":"/api/tree-shell-runtime/mnote-tree-shell-runtime.js"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyFileTreeSelectionAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("loadTreeShellWasmRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceTreeShellRuntimeViaWasm");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceFileTreeSelectionActionWithRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceTreeShellRuntimeWithArtifact");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('mode: "fileTree"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("runtimeArtifact.runtimeApi.reduceEndpoint");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("computeFileTreeSelectionActionResult(action)");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('kind: "update_drop_target"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("updateDropTarget");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchInternalDrop");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchExternalDrop");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchFileTreeDropWithRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("fileTreeInternalDrop");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("fileTreeExternalDrop");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const fallbackRowId = firstItem?.rowId || firstDoc || \"\";");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("replayFileTreeRuntimeHostEvents");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("runtimeResult && fallbackHostEvent.runtimeRequired === true");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("syncFileTreeDropTargetDom");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("row.dataset.dropTarget");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("commitFileTreeRuntimeState");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const rendererFiletreeSelection =");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rendererFiletreeSelection.selectedRowIds");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("computeFileTreeSelectionActionResult");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const selectFileTreeContextRow = (rowId) =>");
|
||||
const fileTreeFallbackRenderBody =
|
||||
iframe?.getAttribute("srcdoc")?.match(/const renderFileTree = \(\) => \{(?<body>[\s\S]*?)const renderNode =/)?.groups
|
||||
?.body ?? "";
|
||||
expect(fileTreeFallbackRenderBody).toContain(
|
||||
'toggleButton.addEventListener("click", (event) => {event.stopPropagation();toggleExpand(item.nodeId);});',
|
||||
);
|
||||
expect(fileTreeFallbackRenderBody).not.toContain(
|
||||
'applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, event.currentTarget);',
|
||||
);
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("guide.pdf");
|
||||
const state = readTreeShellState(iframe?.getAttribute("srcdoc"));
|
||||
expect(state.items).toEqual([
|
||||
@@ -532,20 +607,45 @@ describe("tree-shell-iframe-host", () => {
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"excludedPickerIds":["doc_hidden"]');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_picker_state_reducer_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"contractName":"rust_tree_shell_runtime_artifact_v1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"family":"rust_family"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"executionStrategy":"browser_bridge"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"browserBridge":"iframe_srcdoc"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"wasmModuleUrl":"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('"jsGlueUrl":"/api/tree-shell-runtime/mnote-tree-shell-runtime.js"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyPickerStateAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("loadTreeShellWasmRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceTreeShellRuntimeViaWasm");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reducePickerStateActionWithRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("reduceTreeShellRuntimeWithArtifact");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('mode: "picker"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("buildPickerRuntimeEnvironment");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("runtimeArtifact.runtimeApi.reduceEndpoint");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("pickerPickDocument");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("patchPickerActiveDom");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("getPickablePickerEntries");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("postPickerPickResultToHost");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('tabindex="0"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('tabindex="-1"');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('applyPickerFocusByItemKey("__root__", { focusDom: true })');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("applyPickerFocusByItemKey(item.nodeId, { focusDom: true })");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchPickerPickByItemKeyWithRuntime");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain('dispatchPickerPickByItemKeyWithRuntime("__root__", { focusDom: true })');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("dispatchPickerPickByItemKeyWithRuntime(item.nodeId, { focusDom: true })");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain('applyPickerFocusByItemKey("__root__", { focusDom: true });applyPickerStateAction({ kind: "pick" })');
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain('applyPickerFocusByItemKey(item.nodeId, { focusDom: true });applyPickerStateAction({ kind: "pick" })');
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("const shouldFocusDom = options.focusDom === true");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("if (shouldFocusDom) focusPickerRowElement");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("postPickerPickResultToHost(runtimeResult);");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("if (runtimeResult) {reconcilePickerRuntimeResult(runtimeResult, fallbackResult);return;}commitPickerFocusResult(fallbackResult");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("commitPickerFocusResult(focusResult, { focusDom: focusAction.focusDom });");
|
||||
const bindPickerRootEventsBody =
|
||||
iframe?.getAttribute("srcdoc")?.match(/const bindPickerRootEvents = \(row\) => \{(?<body>[\s\S]*?)\n \};/)?.groups
|
||||
?.body ?? "";
|
||||
expect(bindPickerRootEventsBody).not.toContain("postPickerPickResultToHost(");
|
||||
const bindPickerRowEventsBody =
|
||||
iframe?.getAttribute("srcdoc")?.match(/const bindPickerRowEvents = \(row, item\) => \{(?<body>[\s\S]*?)\n \};/)?.groups
|
||||
?.body ?? "";
|
||||
expect(bindPickerRowEventsBody).not.toContain("handleNavigate(item.nodeId)");
|
||||
expect(bindPickerRowEventsBody).not.toContain("postPickerPickResultToHost(");
|
||||
expect(iframe?.getAttribute("srcdoc")).not.toContain("postPickerPickResultToHost(applyPickerStateAction");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rendererInput.activePickerItem");
|
||||
expect(iframe?.getAttribute("srcdoc")).toContain("rendererInput.excludedPickerIds");
|
||||
const postMessage = vi.fn();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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