feat(tree): complete rust family runtime checklist

- add tree shell runtime artifact contracts and page/filetree/picker runtime reducers
- sink tree.subtree.move write operation through Rust and formalize command event plans
- harden file tree search projection contract and route thin-proxy boundaries
- record completed harness tasks and move design docs into process/done
This commit is contained in:
lix-2026
2026-04-27 10:27:15 +08:00
parent e564dfde02
commit 4ab36a9386
30 changed files with 2502 additions and 432 deletions
@@ -34,6 +34,34 @@ function buildTreeShellUrl(workspaceId, params = {}) {
return `${BASE_URL}/api/tree/shell?${search.toString()}`;
}
function treeHostImplementationUsesIframe(implementation) {
return implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host";
}
async function waitForInlineTreeShellReady(page, surfaceTestId) {
await page.waitForFunction(
(testId) => {
const host = document.querySelector(`[data-testid="${testId}"]`);
if (!(host instanceof HTMLElement) || host.getClientRects().length === 0) {
return false;
}
const implementation = host.getAttribute("data-tree-host-implementation") || "";
if (implementation !== "mnote_web_iframe_proxy" && implementation !== "rust_inline_compat_host") {
return true;
}
const iframe = host.querySelector(`[data-testid="${testId}-rust-iframe"]`);
if (!(iframe instanceof HTMLIFrameElement) || iframe.getClientRects().length === 0) {
return false;
}
const doc = iframe.contentDocument;
const status = doc?.querySelector("#tree-shell-status")?.textContent ?? "";
return Boolean(doc?.querySelector("#tree-shell-state")) && status.includes("就绪");
},
surfaceTestId,
{ timeout: UI_TIMEOUT_MS },
);
}
async function renameFixtureDocuments(requestContext, fixture) {
await renameDocument(requestContext, fixture.workspaceId, fixture.parentId, fixture.parentTitle);
await renameDocument(requestContext, fixture.workspaceId, fixture.childAId, fixture.childATitle);
@@ -133,7 +161,7 @@ async function getPageTreeHostDriver(page) {
return false;
}
const implementation = element.getAttribute("data-tree-host-implementation") || "";
if (implementation === "mnote_web_iframe_proxy") {
if (implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host") {
const iframe = element.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
}
@@ -143,7 +171,8 @@ async function getPageTreeHostDriver(page) {
{ timeout: UI_TIMEOUT_MS },
);
const implementation = await host.getAttribute("data-tree-host-implementation");
if (implementation === "mnote_web_iframe_proxy") {
if (treeHostImplementationUsesIframe(implementation)) {
await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
return {
kind: "iframe",
host,
@@ -163,7 +192,7 @@ async function getFileTreeHostDriver(page) {
return false;
}
const implementation = element.getAttribute("data-tree-host-implementation") || "";
if (implementation === "mnote_web_iframe_proxy") {
if (implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host") {
const iframe = element.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
}
@@ -173,7 +202,8 @@ async function getFileTreeHostDriver(page) {
{ timeout: UI_TIMEOUT_MS },
);
const implementation = await host.getAttribute("data-tree-host-implementation");
if (implementation === "mnote_web_iframe_proxy") {
if (treeHostImplementationUsesIframe(implementation)) {
await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
return {
kind: "iframe",
host,
@@ -837,6 +867,9 @@ async function runPickerDialogChecks(context, fixture) {
await pickerSurface.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const searchInput = dialog.getByPlaceholder("移动到...");
const pickerUsesIframe = (await pickerSurface.locator('[data-testid="tree-picker-surface-rust-iframe"]').count()) > 0;
if (pickerUsesIframe) {
await waitForInlineTreeShellReady(page, "tree-picker-surface");
}
return { dialog, pickerSurface, searchInput, pickerUsesIframe };
};
@@ -986,8 +1019,13 @@ async function runPickerDialogChecks(context, fixture) {
if (!(frame instanceof HTMLIFrameElement)) {
throw new Error("picker iframe 不存在");
}
const escapedNodeId = CSS.escape(nodeId);
const button = frame.contentDocument?.querySelector(
`.tree-row[data-node-id="${nodeId}"] [data-testid="tree-node-open"]`,
[
`.tree-row[data-node-id="${escapedNodeId}"][data-testid="tree-picker-row"]`,
`.tree-row[data-node-id="${escapedNodeId}"] [data-testid="tree-picker-row"]`,
`.tree-row[data-node-id="${escapedNodeId}"] [data-testid="tree-node-open"]`,
].join(", "),
);
if (!button || typeof button.click !== "function") {
throw new Error("picker 目标页面按钮不存在");