feat(tree): close rust family shell cutover
This commit is contained in:
+56
-5
@@ -196,6 +196,32 @@ function getProcessNameByPid(pid) {
|
||||
}
|
||||
}
|
||||
|
||||
function terminatePid(pid) {
|
||||
if (process.platform === "win32") {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(pid, "SIGTERM");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function forceKillPid(pid) {
|
||||
if (process.platform === "win32") {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(pid, "SIGKILL");
|
||||
} catch {
|
||||
// 进程可能已经退出。
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePortFree(port, nameForLog) {
|
||||
const free = await isPortFree("127.0.0.1", port);
|
||||
if (free) return true;
|
||||
@@ -225,7 +251,7 @@ async function ensurePortFree(port, nameForLog) {
|
||||
logPrefix(nameForLog, `检测到端口 ${port} 被占用,准备重启(结束旧进程):${killPids.join(", ")}`);
|
||||
for (const pid of killPids) {
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
||||
terminatePid(pid);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -240,6 +266,20 @@ async function ensurePortFree(port, nameForLog) {
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
for (const pid of killPids) {
|
||||
forceKillPid(pid);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const ok = await isPortFree("127.0.0.1", port, 250);
|
||||
if (ok) return true;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
}
|
||||
|
||||
logPrefix(nameForLog, `端口 ${port} 仍未释放,可能有其他程序占用。`);
|
||||
return false;
|
||||
}
|
||||
@@ -457,7 +497,18 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
logPrefix("system", `启动失败:${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
logPrefix("system", `启动失败:${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensurePortFree,
|
||||
getListeningPidsByPort,
|
||||
getProcessNameByPid,
|
||||
isPortFree,
|
||||
resolveBackendExecutable,
|
||||
terminatePid,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
const assert = require("node:assert");
|
||||
const { spawn } = require("node:child_process");
|
||||
const net = require("node:net");
|
||||
const { test } = require("node:test");
|
||||
const { resolveBackendExecutable, ensurePortFree, isPortFree } = require("./desktop-hot.js");
|
||||
|
||||
function findFreePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("无法分配测试端口")));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function startPythonListener(port) {
|
||||
const pythonBin = resolveBackendExecutable("PYTHON_BIN", "python");
|
||||
const child = spawn(
|
||||
pythonBin,
|
||||
[
|
||||
"-c",
|
||||
`
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind(("127.0.0.1", ${port}))
|
||||
server.listen(16)
|
||||
def accept_loop():
|
||||
while True:
|
||||
conn, _addr = server.accept()
|
||||
conn.close()
|
||||
threading.Thread(target=accept_loop, daemon=True).start()
|
||||
print("ready", flush=True)
|
||||
while True:
|
||||
time.sleep(1)
|
||||
`,
|
||||
],
|
||||
{
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error("测试监听进程启动超时"));
|
||||
}, 3000);
|
||||
|
||||
child.stdout.on("data", (chunk) => {
|
||||
if (chunk.toString("utf8").includes("ready")) {
|
||||
clearTimeout(timeout);
|
||||
resolve(child);
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function waitForExit(child, timeoutMs = 3000) {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
child.off("exit", onExit);
|
||||
reject(new Error(`进程 ${child.pid} 未在 ${timeoutMs}ms 内退出`));
|
||||
}, timeoutMs);
|
||||
const onExit = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
}
|
||||
|
||||
test("ensurePortFree 在非 Windows 平台能释放后端监听进程", async (t) => {
|
||||
if (process.platform === "win32") {
|
||||
t.skip("该用例只覆盖 Linux/macOS 分支");
|
||||
return;
|
||||
}
|
||||
|
||||
const port = await findFreePort();
|
||||
const child = await startPythonListener(port);
|
||||
|
||||
t.after(() => {
|
||||
if (!child.killed) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(await isPortFree("127.0.0.1", port), false);
|
||||
const ok = await ensurePortFree(port, "backend");
|
||||
assert.equal(ok, true);
|
||||
assert.equal(await isPortFree("127.0.0.1", port), true);
|
||||
await waitForExit(child);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,64 @@ const {
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
const ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST =
|
||||
String(process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST || "").trim() === "1";
|
||||
|
||||
function treeHostImplementationUsesIframe(implementation) {
|
||||
return (
|
||||
implementation === "mnote_web_iframe_proxy" ||
|
||||
implementation === "rust_inline_compat_host" ||
|
||||
implementation === "rust_runtime_artifact_host"
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForPickerShellReady(page) {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const host = document.querySelector('[data-testid="tree-picker-surface"]');
|
||||
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" ||
|
||||
implementation === "rust_runtime_artifact_host"
|
||||
) {
|
||||
const iframe = host.querySelector('[data-testid="tree-picker-surface-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="tree-picker-surface-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 },
|
||||
);
|
||||
}
|
||||
|
||||
async function pickerUsesLegacyIframe(pickerSurface) {
|
||||
const implementation = await pickerSurface.getAttribute("data-tree-host-implementation");
|
||||
if (treeHostImplementationUsesIframe(implementation) && !ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST) {
|
||||
throw new Error(
|
||||
`tree-picker-surface 默认主路径不能再使用 legacy iframe host: ${implementation || "unknown"}`,
|
||||
);
|
||||
}
|
||||
return (
|
||||
treeHostImplementationUsesIframe(implementation) &&
|
||||
(await pickerSurface.locator('[data-testid="tree-picker-surface-rust-iframe"]').count()) > 0
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const headless =
|
||||
process.env.MNOTE_SMOKE_HEADLESS === "1"
|
||||
@@ -61,6 +119,10 @@ async function main() {
|
||||
|
||||
const dialog = page.getByRole("dialog");
|
||||
await dialog.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const pickerSurface = dialog.getByTestId("tree-picker-surface");
|
||||
await pickerSurface.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await waitForPickerShellReady(page);
|
||||
const usesLegacyIframe = await pickerUsesLegacyIframe(pickerSurface);
|
||||
const searchInput = dialog.getByPlaceholder("移动到...");
|
||||
const keyboardQuery = `task113-keyboard-${Date.now()}`;
|
||||
|
||||
@@ -92,21 +154,27 @@ async function main() {
|
||||
|
||||
try {
|
||||
await searchInput.fill(keyboardQuery, { timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(frame.contentDocument?.querySelector(`.tree-row[data-node-id="${nodeId}"]`));
|
||||
},
|
||||
fixture.targetId,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
if (usesLegacyIframe) {
|
||||
await page.waitForFunction(
|
||||
(nodeId) => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(frame.contentDocument?.querySelector(`.tree-row[data-node-id="${nodeId}"]`));
|
||||
},
|
||||
fixture.targetId,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
} else {
|
||||
await dialog
|
||||
.locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`)
|
||||
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
await searchInput.focus();
|
||||
await page.waitForFunction(
|
||||
@@ -119,47 +187,61 @@ async function main() {
|
||||
);
|
||||
|
||||
const readFocusedPickerItemKey = async () =>
|
||||
page.evaluate(() => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement)) {
|
||||
return "";
|
||||
}
|
||||
const rootButton = frame.contentDocument?.querySelector(
|
||||
'.tree-row[data-testid="tree-picker-root"][data-focused="true"]',
|
||||
);
|
||||
if (rootButton) {
|
||||
return "__root__";
|
||||
}
|
||||
const row = frame.contentDocument?.querySelector('.tree-row[data-focused="true"]');
|
||||
return row ? row.getAttribute("data-node-id") || "" : "";
|
||||
});
|
||||
usesLegacyIframe
|
||||
? page.evaluate(() => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement) || !frame.contentDocument) {
|
||||
return "";
|
||||
}
|
||||
const rootButton = frame.contentDocument.querySelector(
|
||||
'[data-testid="tree-picker-root"][data-focused="true"]',
|
||||
);
|
||||
if (rootButton) {
|
||||
return "__root__";
|
||||
}
|
||||
const row = frame.contentDocument.querySelector('.tree-row[data-focused="true"]');
|
||||
return row ? row.getAttribute("data-node-id") || "" : "";
|
||||
})
|
||||
: dialog.evaluate((dialogElement) => {
|
||||
const rootButton = dialogElement.querySelector('[data-testid="tree-picker-root"][data-focused="true"]');
|
||||
if (rootButton) {
|
||||
return "__root__";
|
||||
}
|
||||
const row = dialogElement.querySelector('.tree-row[data-focused="true"]');
|
||||
return row ? row.getAttribute("data-node-id") || "" : "";
|
||||
});
|
||||
|
||||
const moveHighlightTo = async (targetNodeId, maxSteps = 4) => {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement)) {
|
||||
return false;
|
||||
}
|
||||
const rootButton = frame.contentDocument?.querySelector(
|
||||
'.tree-row[data-testid="tree-picker-root"][data-focused="true"]',
|
||||
);
|
||||
if (rootButton) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(frame.contentDocument?.querySelector('.tree-row[data-focused="true"]'));
|
||||
},
|
||||
undefined,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
if (usesLegacyIframe) {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement) || !frame.contentDocument) {
|
||||
return false;
|
||||
}
|
||||
const rootButton = frame.contentDocument.querySelector(
|
||||
'[data-testid="tree-picker-root"][data-focused="true"]',
|
||||
);
|
||||
const row = frame.contentDocument.querySelector('.tree-row[data-focused="true"]');
|
||||
return Boolean(rootButton || row);
|
||||
},
|
||||
undefined,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
} else {
|
||||
await dialog
|
||||
.locator('[data-testid="tree-picker-root"][data-focused="true"], .tree-row[data-focused="true"]')
|
||||
.first()
|
||||
.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
for (let index = 0; index < maxSteps; index += 1) {
|
||||
const currentKey = await readFocusedPickerItemKey();
|
||||
@@ -168,19 +250,33 @@ async function main() {
|
||||
}
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await page.waitForFunction(
|
||||
(previousKey) => {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement)) {
|
||||
({ previousKey, legacyIframe }) => {
|
||||
if (legacyIframe) {
|
||||
const frame = Array.from(
|
||||
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
|
||||
)
|
||||
.reverse()
|
||||
.find((element) => element instanceof HTMLIFrameElement) ?? null;
|
||||
if (!(frame instanceof HTMLIFrameElement) || !frame.contentDocument) {
|
||||
return false;
|
||||
}
|
||||
const rootButton = frame.contentDocument.querySelector(
|
||||
'[data-testid="tree-picker-root"][data-focused="true"]',
|
||||
);
|
||||
const row = frame.contentDocument.querySelector('.tree-row[data-focused="true"]');
|
||||
const currentKey = rootButton
|
||||
? "__root__"
|
||||
: row
|
||||
? row.getAttribute("data-node-id") || ""
|
||||
: "";
|
||||
return currentKey !== previousKey;
|
||||
}
|
||||
const dialogElement = document.querySelector('[role="dialog"]');
|
||||
if (!(dialogElement instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
const rootButton = frame.contentDocument?.querySelector(
|
||||
'.tree-row[data-testid="tree-picker-root"][data-focused="true"]',
|
||||
);
|
||||
const row = frame.contentDocument?.querySelector('.tree-row[data-focused="true"]');
|
||||
const rootButton = dialogElement.querySelector('[data-testid="tree-picker-root"][data-focused="true"]');
|
||||
const row = dialogElement.querySelector('.tree-row[data-focused="true"]');
|
||||
const currentKey = rootButton
|
||||
? "__root__"
|
||||
: row
|
||||
@@ -188,7 +284,7 @@ async function main() {
|
||||
: "";
|
||||
return currentKey !== previousKey;
|
||||
},
|
||||
currentKey,
|
||||
{ previousKey: currentKey, legacyIframe: usesLegacyIframe },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
|
||||
Reference in New Issue
Block a user