4-26 树rust-2
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ReactNode } from "react";
|
||||
import React, { type ReactNode } from "react";
|
||||
import { MoveEmbedPickerDialog } from "./move-embed-picker-dialog";
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
@@ -21,6 +21,33 @@ const sidebarData = {
|
||||
trashedDocuments: [],
|
||||
};
|
||||
|
||||
function buildSidebarNode(input: {
|
||||
id: string;
|
||||
title: string;
|
||||
children?: Array<ReturnType<typeof buildSidebarNode>>;
|
||||
}) {
|
||||
return {
|
||||
access_scope: "private",
|
||||
id: input.id,
|
||||
workspace_id: "ws_test",
|
||||
title: input.title,
|
||||
parent_id: null,
|
||||
sort_order: 0,
|
||||
is_starred: false,
|
||||
is_template: false,
|
||||
created_at: "2026-04-24T00:00:00Z",
|
||||
updated_at: "2026-04-24T00:00:00Z",
|
||||
children: input.children ?? [],
|
||||
kernel: {
|
||||
nodeType: "page" as const,
|
||||
depth: 0,
|
||||
position: 0,
|
||||
childCount: input.children?.length ?? 0,
|
||||
expandedByDefault: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: ({ queryKey }: { queryKey: unknown[] }) => {
|
||||
const key = Array.isArray(queryKey) ? queryKey[0] : queryKey;
|
||||
@@ -60,7 +87,9 @@ vi.mock("@/components/ui/dialog", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/input", () => ({
|
||||
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||
Input: React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>((props, ref) => (
|
||||
<input ref={ref} {...props} />
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/tabs", () => ({
|
||||
@@ -94,6 +123,9 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
window.__MNOTE_RUNTIME_CONFIG__ = {
|
||||
treeRendererFamily: "react",
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -101,6 +133,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mockUseDocumentSearch.mockReset();
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
@@ -108,6 +141,7 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
sidebarData.kernelSidebarTree = [];
|
||||
delete window.__MNOTE_RUNTIME_CONFIG__;
|
||||
});
|
||||
|
||||
@@ -141,6 +175,73 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("空查询时应保留根节点并过滤 excludeIds", async () => {
|
||||
sidebarData.kernelSidebarTree = [
|
||||
buildSidebarNode({ id: "doc_hidden", title: "隐藏页面" }),
|
||||
buildSidebarNode({ id: "doc_visible", title: "保留页面" }),
|
||||
];
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot
|
||||
excludeIds={["doc_hidden"]}
|
||||
onPick={vi.fn(async () => undefined)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-testid="tree-picker-root"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_hidden"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="tree-picker-row"][data-node-id="doc_visible"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("空查询态应支持键盘高亮切换并按当前高亮项选中", async () => {
|
||||
sidebarData.kernelSidebarTree = [
|
||||
buildSidebarNode({ id: "doc_first", title: "第一页" }),
|
||||
buildSidebarNode({ id: "doc_second", title: "第二页" }),
|
||||
];
|
||||
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot
|
||||
excludeIds={[]}
|
||||
onPick={onPick}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const input = container.querySelector("input");
|
||||
const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]');
|
||||
expect(input).not.toBeNull();
|
||||
expect(pickerRows).toHaveLength(2);
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(pickerRows[0]?.className ?? "").toContain("bg-[#e3ecff]");
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onPick).toHaveBeenCalledWith("move", "doc_first");
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("搜索结果也应继续复用统一 picker surface", async () => {
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
data: {
|
||||
@@ -198,6 +299,141 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("搜索结果态应支持高亮切换并按当前高亮项选中", async () => {
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: "doc_first",
|
||||
title: "第一页",
|
||||
matchField: "title",
|
||||
},
|
||||
{
|
||||
id: "doc_second",
|
||||
title: "第二页",
|
||||
matchField: "title",
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot
|
||||
excludeIds={[]}
|
||||
onPick={onPick}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const input = container.querySelector("input");
|
||||
expect(input).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
Object.defineProperty(input as HTMLInputElement, "value", {
|
||||
configurable: true,
|
||||
value: "第",
|
||||
});
|
||||
input?.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input?.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
|
||||
const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]');
|
||||
expect(pickerRows).toHaveLength(2);
|
||||
await act(async () => {
|
||||
pickerRows[1]?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(pickerRows[1]?.className ?? "").toContain("bg-[#e3ecff]");
|
||||
|
||||
await act(async () => {
|
||||
pickerRows[1]?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onPick).toHaveBeenCalledWith("move", "doc_second");
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("搜索结果态不应再注入根节点,且仍应支持多次 ArrowDown 后选中后续结果", async () => {
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: "doc_first",
|
||||
title: "第一页",
|
||||
matchField: "title",
|
||||
},
|
||||
{
|
||||
id: "doc_second",
|
||||
title: "第二页",
|
||||
matchField: "title",
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot
|
||||
excludeIds={[]}
|
||||
onPick={onPick}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const input = container.querySelector("input");
|
||||
expect(input).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
Object.defineProperty(input as HTMLInputElement, "value", {
|
||||
configurable: true,
|
||||
value: "第",
|
||||
});
|
||||
input?.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input?.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
|
||||
const pickerRows = container.querySelectorAll('[data-testid="tree-picker-row"]');
|
||||
expect(container.querySelector('[data-testid="tree-picker-root"]')).toBeNull();
|
||||
expect(pickerRows).toHaveLength(2);
|
||||
|
||||
await act(async () => {
|
||||
input?.focus();
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onPick).toHaveBeenCalledWith("move", "doc_second");
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("rust_family 配置下,picker 空态与结果态都应进入统一 host", async () => {
|
||||
window.__MNOTE_RUNTIME_CONFIG__ = {
|
||||
treeRendererFamily: "rust_family",
|
||||
@@ -266,4 +502,165 @@ describe("MoveEmbedPickerDialog", () => {
|
||||
container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("rust_family 配置下,输入框键盘命令应转发给 iframe,并用焦点回传更新 shell 状态", async () => {
|
||||
window.__MNOTE_RUNTIME_CONFIG__ = {
|
||||
treeRendererFamily: "rust_family",
|
||||
};
|
||||
mockUseDocumentSearch.mockReturnValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: "doc_first",
|
||||
title: "第一页",
|
||||
matchField: "title",
|
||||
},
|
||||
{
|
||||
id: "doc_second",
|
||||
title: "第二页",
|
||||
matchField: "title",
|
||||
},
|
||||
],
|
||||
},
|
||||
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" } },
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot
|
||||
excludeIds={[]}
|
||||
onPick={vi.fn(async () => undefined)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const input = container.querySelector("input");
|
||||
expect(input).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
Object.defineProperty(input as HTMLInputElement, "value", {
|
||||
configurable: true,
|
||||
value: "第",
|
||||
});
|
||||
input?.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
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();
|
||||
|
||||
await act(async () => {
|
||||
input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "tree-picker-surface",
|
||||
type: "tree.picker.command",
|
||||
command: "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();
|
||||
|
||||
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",
|
||||
}),
|
||||
"*",
|
||||
);
|
||||
});
|
||||
|
||||
it("rust_family 配置下,无根目录且无结果时也应保持 same-origin host 空态", 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" } },
|
||||
),
|
||||
);
|
||||
|
||||
const onPick = vi.fn(async () => undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MoveEmbedPickerDialog
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
workspaceId="ws_test"
|
||||
defaultMode="move"
|
||||
allowRoot={false}
|
||||
excludeIds={[]}
|
||||
onPick={onPick}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const surface = container.querySelector('[data-testid="tree-picker-surface"]');
|
||||
const iframe = container.querySelector('[data-testid="tree-picker-surface-rust-iframe"]');
|
||||
|
||||
expect(surface?.getAttribute("data-tree-host-kind")).toBe("rust_family");
|
||||
expect(surface?.getAttribute("data-tree-host-implementation")).toBe("mnote_web_iframe_proxy");
|
||||
expect(iframe).not.toBeNull();
|
||||
expect(iframe?.getAttribute("data-tree-shell-inline")).toBe("1");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user