feat(tree): close rust family shell cutover

This commit is contained in:
lix-2026
2026-04-28 16:30:51 +08:00
parent 4ab36a9386
commit 7965c6c107
75 changed files with 9721 additions and 1174 deletions
@@ -23,6 +23,8 @@ const {
const RUN_DIRECT_TREE_SHELL_CHECKS =
String(process.env.MNOTE_TREE_SHELL_DIRECT_SMOKE || "").trim() === "1";
const ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST =
String(process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST || "").trim() === "1";
function buildTreeShellUrl(workspaceId, params = {}) {
const search = new URLSearchParams({
@@ -35,7 +37,19 @@ function buildTreeShellUrl(workspaceId, params = {}) {
}
function treeHostImplementationUsesIframe(implementation) {
return implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host";
return (
implementation === "mnote_web_iframe_proxy" ||
implementation === "rust_inline_compat_host" ||
implementation === "rust_runtime_artifact_host"
);
}
function assertDefaultHostIsDomWasm(implementation, surfaceTestId) {
if (treeHostImplementationUsesIframe(implementation) && !ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST) {
throw new Error(
`${surfaceTestId} 默认主路径不能再使用 legacy iframe host: ${implementation || "unknown"}`,
);
}
}
async function waitForInlineTreeShellReady(page, surfaceTestId) {
@@ -46,16 +60,26 @@ async function waitForInlineTreeShellReady(page, surfaceTestId) {
return false;
}
const implementation = host.getAttribute("data-tree-host-implementation") || "";
if (implementation !== "mnote_web_iframe_proxy" && implementation !== "rust_inline_compat_host") {
return true;
if (
implementation === "mnote_web_iframe_proxy" ||
implementation === "rust_inline_compat_host" ||
implementation === "rust_runtime_artifact_host"
) {
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("就绪");
}
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("就绪");
const domHost = host.querySelector(`[data-testid="${testId}-dom-host"]`);
return (
domHost instanceof HTMLElement &&
domHost.getClientRects().length > 0 &&
domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" &&
domHost.getAttribute("data-tree-dom-shell-ready") === "true"
);
},
surfaceTestId,
{ timeout: UI_TIMEOUT_MS },
@@ -161,16 +185,27 @@ async function getPageTreeHostDriver(page) {
return false;
}
const implementation = element.getAttribute("data-tree-host-implementation") || "";
if (implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host") {
if (
implementation === "mnote_web_iframe_proxy" ||
implementation === "rust_inline_compat_host" ||
implementation === "rust_runtime_artifact_host"
) {
const iframe = element.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
}
return true;
const domHost = element.querySelector('[data-testid="sidebar-page-tree-shell-dom-host"]');
return (
domHost instanceof HTMLElement &&
domHost.getClientRects().length > 0 &&
domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" &&
domHost.getAttribute("data-tree-dom-shell-ready") === "true"
);
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
const implementation = await host.getAttribute("data-tree-host-implementation");
assertDefaultHostIsDomWasm(implementation, "sidebar-page-tree-shell");
if (treeHostImplementationUsesIframe(implementation)) {
await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
return {
@@ -179,7 +214,8 @@ async function getPageTreeHostDriver(page) {
scope: page.frameLocator('[data-testid="sidebar-page-tree-shell-rust-iframe"]'),
};
}
return { kind: "react", host, scope: host };
await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
return { kind: "dom", host, scope: host };
}
async function getFileTreeHostDriver(page) {
@@ -192,16 +228,27 @@ async function getFileTreeHostDriver(page) {
return false;
}
const implementation = element.getAttribute("data-tree-host-implementation") || "";
if (implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host") {
if (
implementation === "mnote_web_iframe_proxy" ||
implementation === "rust_inline_compat_host" ||
implementation === "rust_runtime_artifact_host"
) {
const iframe = element.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
}
return true;
const domHost = element.querySelector('[data-testid="sidebar-file-tree-shell-dom-host"]');
return (
domHost instanceof HTMLElement &&
domHost.getClientRects().length > 0 &&
domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" &&
domHost.getAttribute("data-tree-dom-shell-ready") === "true"
);
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
const implementation = await host.getAttribute("data-tree-host-implementation");
assertDefaultHostIsDomWasm(implementation, "sidebar-file-tree-shell");
if (treeHostImplementationUsesIframe(implementation)) {
await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
return {
@@ -210,7 +257,8 @@ async function getFileTreeHostDriver(page) {
scope: page.frameLocator('[data-testid="sidebar-file-tree-shell-rust-iframe"]'),
};
}
return { kind: "react", host, scope: host };
await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
return { kind: "dom", host, scope: host };
}
async function openPageTreeDocumentFromHost(page, driver, documentId) {
@@ -228,7 +276,7 @@ async function openPageTreeDocumentFromHost(page, driver, documentId) {
});
} else {
await driver.scope
.locator(`[data-testid="page-tree-row"][data-node-id="${documentId}"] [data-testid="page-tree-open"]`)
.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"] [data-testid="tree-node-open"]`)
.click({ timeout: UI_TIMEOUT_MS });
}
const popupPage = await popupPromise;
@@ -252,9 +300,9 @@ async function openPageTreeContextMenuFromHost(page, driver, documentId) {
button.click();
});
} else {
const row = driver.scope.locator(`[data-testid="page-tree-row"][data-node-id="${documentId}"]`);
const row = driver.scope.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`);
await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await row.getByRole("button", { name: "更多操作" }).click({ timeout: UI_TIMEOUT_MS, force: true });
await row.locator('[data-testid="tree-action-menu"]').click({ timeout: UI_TIMEOUT_MS, force: true });
}
await page.getByText("重命名", { exact: true }).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.keyboard.press("Escape");
@@ -270,7 +318,7 @@ async function doubleClickFileTreeDocumentFromHost(page, driver, documentId) {
.dblclick({ timeout: UI_TIMEOUT_MS });
} else {
await driver.scope
.locator(`[data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
.dblclick({ timeout: UI_TIMEOUT_MS });
}
const popupPage = await popupPromise;
@@ -349,11 +397,11 @@ async function waitForPageTreeTitle(page, documentId, title) {
}
await driver.scope
.locator(`[data-testid="page-tree-row"][data-node-id="${documentId}"]`)
.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`)
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
({ nodeId, expectedTitle }) => {
const row = document.querySelector(`[data-testid="page-tree-row"][data-node-id="${nodeId}"]`);
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
return (row?.textContent ?? "").includes(expectedTitle);
},
{ nodeId: documentId, expectedTitle: title },
@@ -387,11 +435,13 @@ async function waitForFileTreeTitle(page, documentId, title) {
}
await driver.scope
.locator(`[data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
({ docId, expectedTitle }) => {
const row = document.querySelector(`[data-testid="filetree-doc-row"][data-doc-id="${docId}"]`);
const row = document.querySelector(
`[data-testid="filetree-doc-row"][data-document-id="${docId}"], [data-testid="filetree-doc-row"][data-doc-id="${docId}"]`,
);
return (row?.textContent ?? "").includes(expectedTitle);
},
{ docId: documentId, expectedTitle: title },
@@ -866,10 +916,12 @@ async function runPickerDialogChecks(context, fixture) {
const pickerSurface = dialog.getByTestId("tree-picker-surface");
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");
}
await waitForInlineTreeShellReady(page, "tree-picker-surface");
const implementation = await pickerSurface.getAttribute("data-tree-host-implementation");
assertDefaultHostIsDomWasm(implementation, "tree-picker-surface");
const pickerUsesIframe =
treeHostImplementationUsesIframe(implementation) &&
(await pickerSurface.locator('[data-testid="tree-picker-surface-rust-iframe"]').count()) > 0;
return { dialog, pickerSurface, searchInput, pickerUsesIframe };
};
@@ -987,7 +1039,7 @@ async function runPickerDialogChecks(context, fixture) {
{ timeout: UI_TIMEOUT_MS },
);
} else {
await dialog
await keyboardDialog
.locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`)
.waitFor({
state: "visible",
@@ -1048,7 +1100,7 @@ async function runPickerDialogChecks(context, fixture) {
pickerSearch: true,
pickerEmpty: true,
pickerSelect: true,
pickerHostKind: keyboardUsesIframe ? "iframe" : "react",
pickerHostKind: keyboardUsesIframe ? "iframe" : "dom",
emptyStateText,
pickerRootVisible,
pickerExcludeCurrentDocument: true,