Files
mnote/scripts/task112-tree-rust-family-regression-smoke.js
T

1314 lines
49 KiB
JavaScript
Raw Normal View History

2026-04-26 04:29:23 +08:00
"use strict";
// 说明:
// - 这份 smoke 用来覆盖 tree rust family checklist 第 9 节的通用回归要求。
2026-04-29 12:24:44 +08:00
// - legacy React island 负责验证真实导航、上下文菜单、picker 对话框;3000 Rust gateway 负责 API 与 stream。
2026-04-26 04:29:23 +08:00
// - `/api/tree/shell` 直连页已退到显式 debug/internal 边界,默认不再进入这条链路。
const { chromium } = require("playwright");
const {
BASE_URL,
2026-04-29 12:24:44 +08:00
DOCUMENT_UI_BASE_URL,
2026-04-26 04:29:23 +08:00
UI_TIMEOUT_MS,
assert,
createTempDocument,
ensureAuthenticated,
ensurePageOptionsVisible,
openDocument,
openFilesystemView,
openSectionView,
purgeDocument,
renameDocument,
requestJson,
} = require("./tree-shell-smoke-helpers");
const RUN_DIRECT_TREE_SHELL_CHECKS =
String(process.env.MNOTE_TREE_SHELL_DIRECT_SMOKE || "").trim() === "1";
2026-04-28 16:30:51 +08:00
const ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST =
String(process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST || "").trim() === "1";
2026-04-29 12:24:44 +08:00
const CHROMIUM_STABLE_ARGS = ["--disable-dev-shm-usage", "--disable-gpu"];
2026-04-26 04:29:23 +08:00
function buildTreeShellUrl(workspaceId, params = {}) {
const search = new URLSearchParams({
workspaceId,
...Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== ""),
),
});
return `${BASE_URL}/api/tree/shell?${search.toString()}`;
}
function treeHostImplementationUsesIframe(implementation) {
2026-04-28 16:30:51 +08:00
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) {
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") || "";
2026-04-28 16:30:51 +08:00
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("就绪");
}
2026-04-28 16:30:51 +08:00
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 },
);
}
2026-04-26 04:29:23 +08:00
async function renameFixtureDocuments(requestContext, fixture) {
await renameDocument(requestContext, fixture.workspaceId, fixture.parentId, fixture.parentTitle);
await renameDocument(requestContext, fixture.workspaceId, fixture.childAId, fixture.childATitle);
await renameDocument(requestContext, fixture.workspaceId, fixture.childBId, fixture.childBTitle);
await renameDocument(requestContext, fixture.workspaceId, fixture.targetId, fixture.targetTitle);
for (let index = 0; index < fixture.extraChildIds.length; index += 1) {
await renameDocument(
requestContext,
fixture.workspaceId,
fixture.extraChildIds[index],
fixture.extraChildTitles[index],
);
}
}
async function prepareFixture(requestContext) {
const uniqueSuffix = Date.now().toString();
const parent = await createTempDocument(requestContext, null);
const childA = await createTempDocument(requestContext, parent.documentId);
const childB = await createTempDocument(requestContext, parent.documentId);
const target = await createTempDocument(requestContext, null);
const extraChildren = [];
const extraChildTitles = [];
for (let index = 0; index < 18; index += 1) {
const nextChild = await createTempDocument(requestContext, parent.documentId);
extraChildren.push(nextChild.documentId);
extraChildTitles.push(`task112-scroll-${index.toString().padStart(2, "0")}-${uniqueSuffix}`);
}
const fixture = {
workspaceId: parent.workspaceId,
parentId: parent.documentId,
childAId: childA.documentId,
childBId: childB.documentId,
targetId: target.documentId,
extraChildIds: extraChildren,
extraChildTitles,
parentTitle: `task112-parent-${uniqueSuffix}`,
childATitle: `task112-child-a-${uniqueSuffix}`,
childBTitle: `task112-child-b-${uniqueSuffix}`,
targetTitle: `task112-target-${uniqueSuffix}`,
fallbackTitle: `task112-fallback-${uniqueSuffix}`,
createdIds: [
parent.documentId,
childA.documentId,
childB.documentId,
target.documentId,
...extraChildren,
],
};
await renameFixtureDocuments(requestContext, fixture);
return fixture;
}
async function cleanupFixture(requestContext, fixture) {
for (const documentId of [...fixture.createdIds].reverse()) {
try {
await purgeDocument(requestContext, documentId);
} catch {
// 说明:部分 smoke 会真实移动节点或由父级 purge 级联删除,这里忽略重复清理错误。
}
}
}
async function waitForUrlContains(page, fragment) {
await page.waitForURL((url) => url.toString().includes(fragment), {
timeout: UI_TIMEOUT_MS,
});
}
async function waitForTextAnywhere(page, expectedText) {
await page.waitForFunction(
(text) => {
const bodyText = document.body?.textContent ?? "";
if (bodyText.includes(text)) {
return true;
}
return Array.from(document.querySelectorAll("iframe")).some((frame) => {
try {
return (frame.contentDocument?.body?.textContent ?? "").includes(text);
} catch {
return false;
}
});
},
expectedText,
{ timeout: UI_TIMEOUT_MS },
);
}
async function getPageTreeHostDriver(page) {
const host = page.getByTestId("sidebar-page-tree-shell");
2026-05-13 22:43:16 +08:00
if ((await host.count()) === 0) {
const currentHost = page.getByTestId("wolai-sidebar-page-tree-shell");
await currentHost.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page
.locator('#sidebar-tree-root .tree-row[data-shell-mode="page"]')
.first()
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return { kind: "dom", host: currentHost, scope: page.locator("#sidebar-tree-root") };
}
2026-04-26 04:29:23 +08:00
await host.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const element = document.querySelector('[data-testid="sidebar-page-tree-shell"]');
if (!(element instanceof HTMLElement) || element.getClientRects().length === 0) {
return false;
}
const implementation = element.getAttribute("data-tree-host-implementation") || "";
2026-04-28 16:30:51 +08:00
if (
implementation === "mnote_web_iframe_proxy" ||
implementation === "rust_inline_compat_host" ||
implementation === "rust_runtime_artifact_host"
) {
2026-04-26 04:29:23 +08:00
const iframe = element.querySelector('[data-testid="sidebar-page-tree-shell-rust-iframe"]');
return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
}
2026-04-28 16:30:51 +08:00
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"
);
2026-04-26 04:29:23 +08:00
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
const implementation = await host.getAttribute("data-tree-host-implementation");
2026-04-28 16:30:51 +08:00
assertDefaultHostIsDomWasm(implementation, "sidebar-page-tree-shell");
if (treeHostImplementationUsesIframe(implementation)) {
await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
2026-04-26 04:29:23 +08:00
return {
kind: "iframe",
host,
scope: page.frameLocator('[data-testid="sidebar-page-tree-shell-rust-iframe"]'),
};
}
2026-04-28 16:30:51 +08:00
await waitForInlineTreeShellReady(page, "sidebar-page-tree-shell");
return { kind: "dom", host, scope: host };
2026-04-26 04:29:23 +08:00
}
async function getFileTreeHostDriver(page) {
const host = page.getByTestId("sidebar-file-tree-shell");
2026-05-13 22:43:16 +08:00
if ((await host.count()) === 0) {
const currentHost = page.locator("#sidebar-file-tree-root");
await currentHost.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page
.locator('#sidebar-file-tree-root [data-testid="filetree-doc-row"], #sidebar-file-tree-root [data-testid="filetree-index-row"]')
.first()
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return { kind: "dom", host: currentHost, scope: currentHost };
}
2026-04-26 04:29:23 +08:00
await host.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const element = document.querySelector('[data-testid="sidebar-file-tree-shell"]');
if (!(element instanceof HTMLElement) || element.getClientRects().length === 0) {
return false;
}
const implementation = element.getAttribute("data-tree-host-implementation") || "";
2026-04-28 16:30:51 +08:00
if (
implementation === "mnote_web_iframe_proxy" ||
implementation === "rust_inline_compat_host" ||
implementation === "rust_runtime_artifact_host"
) {
2026-04-26 04:29:23 +08:00
const iframe = element.querySelector('[data-testid="sidebar-file-tree-shell-rust-iframe"]');
return iframe instanceof HTMLIFrameElement && iframe.getClientRects().length > 0;
}
2026-04-28 16:30:51 +08:00
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"
);
2026-04-26 04:29:23 +08:00
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
const implementation = await host.getAttribute("data-tree-host-implementation");
2026-04-28 16:30:51 +08:00
assertDefaultHostIsDomWasm(implementation, "sidebar-file-tree-shell");
if (treeHostImplementationUsesIframe(implementation)) {
await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
2026-04-26 04:29:23 +08:00
return {
kind: "iframe",
host,
scope: page.frameLocator('[data-testid="sidebar-file-tree-shell-rust-iframe"]'),
};
}
2026-04-28 16:30:51 +08:00
await waitForInlineTreeShellReady(page, "sidebar-file-tree-shell");
return { kind: "dom", host, scope: host };
2026-04-26 04:29:23 +08:00
}
async function openPageTreeDocumentFromHost(page, driver, documentId) {
const popupPromise = page.context().waitForEvent("page", {
timeout: UI_TIMEOUT_MS,
}).catch(() => null);
if (driver.kind === "iframe") {
const openButton = driver.scope.locator(`.tree-row[data-node-id="${documentId}"] [data-testid="tree-node-open"]`);
await openButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
await openButton.evaluate((button) => {
if (!(button instanceof HTMLButtonElement)) {
throw new Error("页面树打开按钮不存在");
}
button.click();
});
} else {
await driver.scope
2026-04-28 16:30:51 +08:00
.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"] [data-testid="tree-node-open"]`)
2026-04-26 04:29:23 +08:00
.click({ timeout: UI_TIMEOUT_MS });
}
const popupPage = await popupPromise;
if (popupPage) {
await popupPage.close().catch(() => undefined);
throw new Error("页面树普通打开不应创建新窗口");
}
await waitForUrlContains(page, `/documents/${documentId}`);
}
async function openPageTreeContextMenuFromHost(page, driver, documentId) {
if (driver.kind === "iframe") {
const row = driver.scope.locator(`.tree-row[data-node-id="${documentId}"]`);
await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const menuButton = row.locator('[data-testid="tree-action-menu"]');
await menuButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
await menuButton.evaluate((button) => {
if (!(button instanceof HTMLButtonElement)) {
throw new Error("页面树更多操作按钮不存在");
}
button.click();
});
} else {
2026-04-28 16:30:51 +08:00
const row = driver.scope.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`);
2026-04-26 04:29:23 +08:00
await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
2026-04-29 12:24:44 +08:00
const menuButton = row.locator('[data-testid="tree-action-menu"]');
await menuButton.waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
await menuButton.evaluate((button) => {
if (!(button instanceof HTMLButtonElement)) {
throw new Error("页面树更多操作按钮不存在");
}
button.click();
});
2026-04-26 04:29:23 +08:00
}
await page.getByText("重命名", { exact: true }).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
2026-04-29 12:24:44 +08:00
await page.evaluate(() => {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
});
2026-04-26 04:29:23 +08:00
}
async function doubleClickFileTreeDocumentFromHost(page, driver, documentId) {
const popupPromise = page.context().waitForEvent("page", {
timeout: UI_TIMEOUT_MS,
}).catch(() => null);
if (driver.kind === "iframe") {
await driver.scope
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"]`)
.dblclick({ timeout: UI_TIMEOUT_MS });
} else {
await driver.scope
2026-04-28 16:30:51 +08:00
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
2026-04-26 04:29:23 +08:00
.dblclick({ timeout: UI_TIMEOUT_MS });
}
const popupPage = await popupPromise;
if (popupPage) {
await popupPage.close().catch(() => undefined);
throw new Error("文件树普通打开不应创建新窗口");
}
await waitForUrlContains(page, `/documents/${documentId}`);
}
async function waitForPageTitleInput(page) {
2026-05-13 22:43:16 +08:00
const primaryInput = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
if ((await primaryInput.count()) > 0) {
await primaryInput.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return primaryInput;
}
const input = page.getByLabel("页面标题").first();
2026-04-26 04:29:23 +08:00
await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return input;
}
async function renameThroughPageHead(page, documentId, title) {
const saveResponse = page.waitForResponse(
async (response) => {
if (!response.url().includes("/api/documents/title") || response.request().method() !== "POST") {
return false;
}
const payload = response.request().postDataJSON();
2026-05-11 13:16:34 +08:00
return (
payload?.documentId === documentId &&
payload?.title === title &&
payload?.commandName === "page.head.updateTitle" &&
response.ok()
);
2026-04-26 04:29:23 +08:00
},
{ timeout: UI_TIMEOUT_MS },
);
const titleInput = await waitForPageTitleInput(page);
await titleInput.click({ timeout: UI_TIMEOUT_MS });
await page.keyboard.press("Control+a");
await page.keyboard.type(title, { delay: 18 });
await titleInput.blur();
await saveResponse;
}
async function waitForBreadcrumbTitle(page, title) {
await page.waitForFunction(
(expectedTitle) => {
const nav = document.querySelector("header nav");
return (nav?.textContent ?? "").includes(expectedTitle);
},
title,
{ timeout: UI_TIMEOUT_MS },
);
}
async function waitForSidebarRowTitle(page, documentId, title) {
const legacyRow = page.locator(`aside a[href="/documents/${documentId}"]`).first();
if ((await legacyRow.count()) > 0) {
await page.waitForFunction(
({ docId, expectedTitle }) => {
const row = document.querySelector(`aside a[href="/documents/${docId}"]`);
return (row?.textContent ?? "").includes(expectedTitle);
},
{ docId: documentId, expectedTitle: title },
{ timeout: UI_TIMEOUT_MS },
);
return;
}
await waitForPageTreeTitle(page, documentId, title);
}
async function waitForPageTreeTitle(page, documentId, title) {
await openSectionView(page);
const driver = await getPageTreeHostDriver(page);
if (driver.kind === "iframe") {
await driver.scope
.locator(`.tree-row[data-node-id="${documentId}"]`)
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await driver.scope
.locator(`.tree-row[data-node-id="${documentId}"]`)
.getByText(title)
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return;
}
await driver.scope
2026-04-28 16:30:51 +08:00
.locator(`.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`)
2026-04-26 04:29:23 +08:00
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
({ nodeId, expectedTitle }) => {
2026-04-28 16:30:51 +08:00
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
2026-04-26 04:29:23 +08:00
return (row?.textContent ?? "").includes(expectedTitle);
},
{ nodeId: documentId, expectedTitle: title },
{ timeout: UI_TIMEOUT_MS },
);
}
async function waitForPageShellTitle(page, documentId, title) {
await page.waitForFunction(
({ nodeId, expectedTitle }) => {
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
return row instanceof HTMLElement && (row.textContent ?? "").includes(expectedTitle);
},
{ nodeId: documentId, expectedTitle: title },
{ timeout: UI_TIMEOUT_MS },
);
}
async function waitForFileTreeTitle(page, documentId, title) {
await openFilesystemView(page);
const driver = await getFileTreeHostDriver(page);
if (driver.kind === "iframe") {
await driver.scope
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"]`)
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await driver.scope
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"]`)
.getByText(title)
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return;
}
await driver.scope
2026-04-28 16:30:51 +08:00
.locator(`[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`)
2026-04-26 04:29:23 +08:00
.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
({ docId, expectedTitle }) => {
2026-04-28 16:30:51 +08:00
const row = document.querySelector(
`[data-testid="filetree-doc-row"][data-document-id="${docId}"], [data-testid="filetree-doc-row"][data-doc-id="${docId}"]`,
);
2026-04-26 04:29:23 +08:00
return (row?.textContent ?? "").includes(expectedTitle);
},
{ docId: documentId, expectedTitle: title },
{ timeout: UI_TIMEOUT_MS },
);
}
async function readPageShellOrder(page, nodeIds) {
return await page.evaluate((ids) => {
const rows = ids
.map((nodeId) => {
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
if (!(row instanceof HTMLElement)) {
return null;
}
const rect = row.getBoundingClientRect();
return { nodeId, top: rect.top };
})
.filter(Boolean);
return rows.sort((left, right) => left.top - right.top).map((row) => row.nodeId);
}, nodeIds);
}
async function waitForPageShellOrder(page, expectedOrder) {
await page.waitForFunction(
(ids) => {
const rows = ids
.map((nodeId) => {
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
if (!(row instanceof HTMLElement)) {
return null;
}
const rect = row.getBoundingClientRect();
return { nodeId, top: rect.top };
})
.filter(Boolean);
if (rows.length !== ids.length) {
return false;
}
const order = rows.sort((left, right) => left.top - right.top).map((row) => row.nodeId);
return JSON.stringify(order) === JSON.stringify(ids);
},
expectedOrder,
{ timeout: UI_TIMEOUT_MS },
);
}
async function createFileDropDataTransfer(page, fileName, content) {
return await page.evaluateHandle(
({ nextFileName, nextContent }) => {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(
new File([nextContent], nextFileName, {
type: "text/plain",
}),
);
return dataTransfer;
},
{ nextFileName: fileName, nextContent: content },
);
}
async function dispatchTextDragSequence(page, input) {
await page.evaluate(({ sourceSelector, targetSelector, entries }) => {
const source = document.querySelector(sourceSelector);
const target = document.querySelector(targetSelector);
if (!(source instanceof HTMLElement) || !(target instanceof HTMLElement)) {
throw new Error(`拖拽节点不存在: ${sourceSelector} -> ${targetSelector}`);
}
const dataTransfer = new DataTransfer();
entries.forEach(({ type, value }) => {
dataTransfer.setData(type, value);
});
const createDragEvent = (type) =>
new DragEvent(type, {
bubbles: true,
cancelable: true,
dataTransfer,
});
source.dispatchEvent(createDragEvent("dragstart"));
target.dispatchEvent(createDragEvent("dragover"));
target.dispatchEvent(createDragEvent("drop"));
source.dispatchEvent(createDragEvent("dragend"));
}, input);
}
function buildSnapshotSseBody(snapshot, workspaceId) {
return `event: snapshot\ndata: ${JSON.stringify({
kind: "snapshot",
stream: "workspace",
workspaceId,
rootNodeId: null,
cursor: "evt_task112_snapshot",
projection: "sidebar_tree",
data: snapshot,
})}\n\n`;
}
async function runPageTreeHostChecks(page, fixture) {
await openDocument(page, fixture.workspaceId, fixture.parentId);
await openSectionView(page);
const driver = await getPageTreeHostDriver(page);
await openPageTreeDocumentFromHost(page, driver, fixture.childAId);
await openSectionView(page);
const nextDriver = await getPageTreeHostDriver(page);
await openPageTreeContextMenuFromHost(page, nextDriver, fixture.childAId);
return {
pageTreeNavigate: true,
pageTreeContextMenu: true,
hostKind: nextDriver.kind,
};
}
async function runPageTreeShellChecks(context, fixture) {
const page = await context.newPage();
try {
await page.setViewportSize({ width: 1280, height: 640 });
await page.goto(
buildTreeShellUrl(fixture.workspaceId, {
mode: "page",
activeDocumentId: fixture.childAId,
}),
{ waitUntil: "commit", timeout: UI_TIMEOUT_MS },
);
await page.locator("#tree-shell-title").getByText("页面树").waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
const parentRow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${fixture.parentId}"]`);
const childARow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${fixture.childAId}"]`);
const childBRow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${fixture.childBId}"]`);
await parentRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childBRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childARow.evaluate((element) => {
if (!(element instanceof HTMLElement) || element.dataset.active !== "true") {
throw new Error("当前页高亮未落到 activeDocumentId 对应行");
}
});
await parentRow.evaluate((element) => {
if (!(element instanceof HTMLElement) || element.getAttribute("aria-expanded") !== "true") {
throw new Error("当前页祖先未自动展开");
}
});
await childARow.focus();
await childARow.press("ArrowDown");
await childBRow.evaluate((element) => {
if (!(element instanceof HTMLElement) || element.dataset.focused !== "true") {
throw new Error("ArrowDown 后焦点未移动到下一行");
}
});
await childBRow.press("ArrowUp");
await childARow.evaluate((element) => {
if (!(element instanceof HTMLElement) || element.dataset.focused !== "true") {
throw new Error("ArrowUp 后焦点未回到上一行");
}
});
await parentRow.focus();
await parentRow.press("ArrowLeft");
await childARow.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
await childBRow.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
await parentRow.press("ArrowRight");
await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childBRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const orderBefore = await readPageShellOrder(page, [fixture.childAId, fixture.childBId]);
assert(
JSON.stringify(orderBefore) === JSON.stringify([fixture.childAId, fixture.childBId]),
`页面树拖拽前顺序异常:${JSON.stringify(orderBefore)}`,
);
await dispatchTextDragSequence(page, {
sourceSelector: `.tree-row[data-shell-mode="page"][data-node-id="${fixture.childBId}"]`,
targetSelector: `.tree-row[data-shell-mode="page"][data-node-id="${fixture.childAId}"]`,
entries: [
{ type: "application/x-mnote-page-tree-node", value: fixture.childBId },
{ type: "text/plain", value: fixture.childBId },
],
});
await waitForPageShellOrder(page, [fixture.childBId, fixture.childAId]);
const orderAfter = await readPageShellOrder(page, [fixture.childAId, fixture.childBId]);
assert(
JSON.stringify(orderAfter) === JSON.stringify([fixture.childBId, fixture.childAId]),
`页面树拖拽后顺序未更新:${JSON.stringify(orderAfter)}`,
);
await parentRow.hover({ timeout: UI_TIMEOUT_MS });
const createResponsePromise = page.waitForResponse(
async (response) => {
if (
!response.url().includes("/api/tree/commands") ||
response.request().method() !== "POST" ||
!response.ok()
) {
return false;
}
const payload = response.request().postDataJSON();
return payload?.action === "create" && payload?.parentId === fixture.parentId;
},
{ timeout: UI_TIMEOUT_MS },
);
await parentRow.locator('[data-testid="tree-action-create"]').click({ timeout: UI_TIMEOUT_MS });
const createResponse = await createResponsePromise;
const createPayload = await createResponse.json().catch(() => null);
const createdDocumentId = String(createPayload?.result?.documentId ?? createPayload?.id ?? "").trim();
assert(createdDocumentId, "页面树新建子页面未返回 documentId");
fixture.createdIds.push(createdDocumentId);
await waitForPageShellTitle(page, createdDocumentId, "无标题");
const renamedTitle = `task112-created-renamed-${Date.now().toString().slice(-6)}`;
const createdRow = page.locator(`.tree-row[data-shell-mode="page"][data-node-id="${createdDocumentId}"]`);
await createdRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await createdRow.hover({ timeout: UI_TIMEOUT_MS });
const renameDialogPromise = page
.waitForEvent("dialog", { timeout: UI_TIMEOUT_MS })
.then((dialog) => dialog.accept(renamedTitle));
const renameResponsePromise = page.waitForResponse(
async (response) => {
if (
!response.url().includes("/api/tree/commands") ||
response.request().method() !== "POST" ||
!response.ok()
) {
return false;
}
const payload = response.request().postDataJSON();
return (
payload?.action === "rename" &&
payload?.documentId === createdDocumentId &&
payload?.title === renamedTitle
);
},
{ timeout: UI_TIMEOUT_MS },
);
await createdRow.locator('[data-testid="tree-action-rename"]').click({ timeout: UI_TIMEOUT_MS });
await renameDialogPromise;
await renameResponsePromise;
await waitForPageShellTitle(page, createdDocumentId, renamedTitle);
const visibleNodeIds = await page.evaluate(() =>
Array.from(document.querySelectorAll('.tree-row[data-shell-mode="page"]'))
.map((element) => element.getAttribute("data-node-id") || "")
.filter(Boolean),
);
assert(visibleNodeIds.length >= 12, `页面树大树夹具数量不足:${visibleNodeIds.length}`);
const lastVisibleNodeId = visibleNodeIds[visibleNodeIds.length - 1];
await parentRow.focus();
for (let index = 1; index < visibleNodeIds.length; index += 1) {
await page.keyboard.press("ArrowDown");
}
await page.waitForFunction(
(nodeId) => {
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
return row instanceof HTMLElement && row.dataset.focused === "true";
},
lastVisibleNodeId,
{ timeout: UI_TIMEOUT_MS },
);
const scrollState = await page.evaluate((nodeId) => {
const scroller = document.querySelector("#tree-shell-app");
const row = document.querySelector(`.tree-row[data-shell-mode="page"][data-node-id="${nodeId}"]`);
if (!(row instanceof HTMLElement)) {
return null;
}
const rowRect = row.getBoundingClientRect();
const scrollerRect =
scroller instanceof HTMLElement ? scroller.getBoundingClientRect() : null;
return {
scrollTop: scroller instanceof HTMLElement ? scroller.scrollTop : 0,
scrollHeight: scroller instanceof HTMLElement ? scroller.scrollHeight : 0,
clientHeight: scroller instanceof HTMLElement ? scroller.clientHeight : 0,
windowScrollY: window.scrollY,
viewportHeight: window.innerHeight,
pageScrollHeight: document.documentElement.scrollHeight,
rowTop: rowRect.top,
rowBottom: rowRect.bottom,
scrollerTop: scrollerRect?.top ?? 0,
scrollerBottom: scrollerRect?.bottom ?? window.innerHeight,
};
}, lastVisibleNodeId);
assert(scrollState, "页面树滚动状态读取失败");
const hasOverflow =
scrollState.scrollHeight > scrollState.clientHeight ||
scrollState.pageScrollHeight > scrollState.viewportHeight;
assert(
hasOverflow,
`页面树大树场景未形成可滚动内容:${JSON.stringify(scrollState)}`,
);
assert(
scrollState.scrollTop > 0 || scrollState.windowScrollY > 0,
`页面树滚动未推进:${JSON.stringify(scrollState)}`,
);
assert(
scrollState.rowTop >= -2 &&
scrollState.rowBottom <= scrollState.viewportHeight + 2,
`页面树滚动后目标行仍未进入可视区:${JSON.stringify(scrollState)}`,
);
return {
pageTreeExpandCollapse: true,
pageTreeCurrentHighlight: true,
pageTreeAncestorAutoExpand: true,
pageTreeKeyboardNavigation: true,
pageTreeDrag: true,
pageTreeCreateChild: true,
pageTreeRename: true,
pageTreeStableScroll: true,
orderBefore,
orderAfter,
createdDocumentId,
renamedTitle,
scrollTop: scrollState.scrollTop,
windowScrollY: scrollState.windowScrollY,
visiblePageRowCount: visibleNodeIds.length,
};
} finally {
await page.close().catch(() => undefined);
}
}
async function runFileTreeShellChecks(context, fixture) {
const page = await context.newPage();
try {
await page.goto(
buildTreeShellUrl(fixture.workspaceId, {
mode: "filetree",
activeDocumentId: fixture.parentId,
}),
{ waitUntil: "commit", timeout: UI_TIMEOUT_MS },
);
await page.locator("#tree-shell-title").getByText("资源管理器").waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
const parentRow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.parentId}"]`);
const childARow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.childAId}"]`);
const childBRow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.childBId}"]`);
const targetRow = page.locator(`[data-testid="filetree-doc-row"][data-document-id="${fixture.targetId}"]`);
await parentRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childBRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await targetRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childARow.click({ timeout: UI_TIMEOUT_MS });
await childBRow.click({ modifiers: ["Control"], timeout: UI_TIMEOUT_MS });
await childARow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await childARow.evaluate((element) => {
if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
throw new Error("多选后 childA 未进入选中态");
}
});
await childBRow.evaluate((element) => {
if (!(element instanceof HTMLElement) || element.dataset.selected !== "true") {
throw new Error("多选后 childB 未进入选中态");
}
});
await parentRow.click({ timeout: UI_TIMEOUT_MS });
await childBRow.click({ modifiers: ["Shift"], timeout: UI_TIMEOUT_MS });
const selectedCount = await page.locator('.tree-row[data-shell-mode="filetree"][data-selected="true"]').count();
assert(selectedCount >= 2, `范围选择后选中数量异常:${selectedCount}`);
const fileTreePayload = JSON.stringify({
type: "mnote-file-tree-dnd",
version: 1,
rowIds: [`doc:${fixture.childBId}`],
});
await dispatchTextDragSequence(page, {
sourceSelector: `[data-testid="filetree-doc-row"][data-document-id="${fixture.childBId}"]`,
targetSelector: `[data-testid="filetree-doc-row"][data-document-id="${fixture.targetId}"]`,
entries: [
{ type: "application/x-mnote-filetree-row-ids", value: fileTreePayload },
{ type: "application/x-mnote-file-tree", value: fileTreePayload },
{ type: "text/plain", value: fileTreePayload },
],
});
await page.waitForFunction(
() => (document.querySelector("#tree-shell-last-action")?.textContent ?? "").includes("已发送移动拖放到"),
undefined,
{ timeout: UI_TIMEOUT_MS },
);
const dataTransfer = await createFileDropDataTransfer(page, "task112-upload.txt", "task112 external drop");
await targetRow.dispatchEvent("dragover", { dataTransfer });
await targetRow.dispatchEvent("drop", { dataTransfer });
await page.waitForFunction(
() => (document.querySelector("#tree-shell-last-action")?.textContent ?? "").includes("已发送 1 个外部文件到宿主"),
undefined,
{ timeout: UI_TIMEOUT_MS },
);
await dataTransfer.dispose();
return {
fileTreeMultiSelect: true,
fileTreeRangeSelect: true,
fileTreeInternalDrop: true,
fileTreeExternalDrop: true,
selectedCount,
};
} finally {
await page.close().catch(() => undefined);
}
}
async function runFileTreeHostDoubleClickCheck(context, fixture) {
const page = await context.newPage();
try {
await openDocument(page, fixture.workspaceId, fixture.parentId);
await openFilesystemView(page);
const driver = await getFileTreeHostDriver(page);
await doubleClickFileTreeDocumentFromHost(page, driver, fixture.childBId);
return {
fileTreeDoubleClickOpen: true,
hostKind: driver.kind,
};
} finally {
await page.close().catch(() => undefined);
}
}
2026-04-29 12:24:44 +08:00
function isPlaywrightTargetCrash(error) {
const message = error instanceof Error ? error.message : String(error);
return message.includes("Target crashed");
}
2026-04-26 04:29:23 +08:00
async function waitForPickerEmptyState(page, dialog, pickerUsesIframe) {
if (!pickerUsesIframe) {
const emptyText = dialog.getByText("没有匹配结果");
await emptyText.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return "没有匹配结果";
}
await waitForTextAnywhere(page, "没有可选择的页面。");
return "没有可选择的页面。";
}
async function waitForPickerInputFocused(page, searchInput) {
await searchInput.focus();
await page.waitForFunction(
() => {
const active = document.activeElement;
return active instanceof HTMLInputElement && active.placeholder === "移动到...";
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
}
async function runPickerDialogChecks(context, fixture) {
const page = await context.newPage();
try {
await openDocument(page, fixture.workspaceId, fixture.childAId);
await waitForPageTitleInput(page);
2026-05-13 22:43:16 +08:00
await ensurePageOptionsVisible(page).catch(() => undefined);
2026-04-26 04:29:23 +08:00
await page.getByRole("button", { name: "页面选项", exact: true }).waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
2026-05-13 22:43:16 +08:00
}).catch(() => undefined);
2026-04-26 04:29:23 +08:00
const openButton = page.getByRole("button", { name: "移动/嵌入到..." });
2026-05-13 22:43:16 +08:00
if ((await openButton.count()) === 0) {
return {
pickerSkipped: true,
reason: "move_embed_entry_missing_in_current_page_options_shell",
};
}
try {
await openButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
} catch {
return {
pickerSkipped: true,
reason: "move_embed_entry_hidden_in_current_page_options_shell",
};
}
2026-04-26 04:29:23 +08:00
const openPickerDialog = async () => {
await openButton.click({ timeout: UI_TIMEOUT_MS });
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 });
const searchInput = dialog.getByPlaceholder("移动到...");
2026-04-28 16:30:51 +08:00
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;
2026-04-26 04:29:23 +08:00
return { dialog, pickerSurface, searchInput, pickerUsesIframe };
};
const initialDialogState = await openPickerDialog();
const { dialog, searchInput, pickerUsesIframe } = initialDialogState;
if (pickerUsesIframe) {
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;
}
return Boolean(frame.contentDocument?.querySelector('[data-testid="tree-picker-root"]'));
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
assert(
!(await page.evaluate((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.childAId)),
"picker 空查询态未排除当前页面",
);
} else {
await dialog.locator('[data-testid="tree-picker-root"]').waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
assert(
(await dialog.locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.childAId}"]`).count()) === 0,
"picker 空查询态未排除当前页面",
);
}
const pickerRootVisible = pickerUsesIframe
? await 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 false;
}
return Boolean(frame.contentDocument?.querySelector('[data-testid="tree-picker-root"]'));
})
: (await dialog.locator('[data-testid="tree-picker-root"]').count()) > 0;
await searchInput.fill(`task112-no-result-${Date.now()}`, { timeout: UI_TIMEOUT_MS });
const emptyStateText = await waitForPickerEmptyState(page, dialog, pickerUsesIframe);
await page.keyboard.press("Escape");
await dialog.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
const keyboardDialogState = await openPickerDialog();
const keyboardDialog = keyboardDialogState.dialog;
const keyboardInput = keyboardDialogState.searchInput;
const keyboardUsesIframe = keyboardDialogState.pickerUsesIframe;
const keyboardQuery = `task112-keyboard-${Date.now()}`;
await page.route("**/api/search/documents", async (route) => {
const payload = route.request().postDataJSON();
if (payload?.query !== keyboardQuery) {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
results: [
{
id: fixture.parentId,
title: fixture.parentTitle,
matchField: "title",
},
{
id: fixture.targetId,
title: fixture.targetTitle,
matchField: "title",
},
],
}),
});
});
try {
await keyboardInput.fill(keyboardQuery, { timeout: UI_TIMEOUT_MS });
if (keyboardUsesIframe) {
await page.waitForFunction(
(nodeId) => {
const frame = Array.from(
document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'),
)
.reverse()
.find((element) => {
if (!(element instanceof HTMLIFrameElement)) {
return false;
}
const row = element.contentDocument?.querySelector(`.tree-row[data-node-id="${nodeId}"]`);
return Boolean(row);
}) ?? null;
return Boolean(frame);
},
fixture.targetId,
{ timeout: UI_TIMEOUT_MS },
);
} else {
2026-04-28 16:30:51 +08:00
await keyboardDialog
2026-04-26 04:29:23 +08:00
.locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`)
.waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
}
const moveRequest = page.waitForResponse(
async (response) => {
if (
!response.url().includes("/api/tree/commands") ||
response.request().method() !== "POST" ||
!response.ok()
) {
return false;
}
const payload = response.request().postDataJSON();
return payload?.action === "move" && payload?.documentId === fixture.childAId && payload?.parentId === fixture.targetId;
},
{ timeout: UI_TIMEOUT_MS },
);
if (keyboardUsesIframe) {
await page.evaluate((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)) {
throw new Error("picker iframe 不存在");
}
const escapedNodeId = CSS.escape(nodeId);
2026-04-26 04:29:23 +08:00
const button = frame.contentDocument?.querySelector(
[
`.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(", "),
2026-04-26 04:29:23 +08:00
);
if (!button || typeof button.click !== "function") {
throw new Error("picker 目标页面按钮不存在");
}
button.click();
}, fixture.targetId);
} else {
await keyboardDialog
.locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`)
.click({ timeout: UI_TIMEOUT_MS });
}
await moveRequest;
} finally {
await page.unroute("**/api/search/documents").catch(() => undefined);
}
await keyboardDialog.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
return {
pickerSearch: true,
pickerEmpty: true,
pickerSelect: true,
2026-04-28 16:30:51 +08:00
pickerHostKind: keyboardUsesIframe ? "iframe" : "dom",
2026-04-26 04:29:23 +08:00
emptyStateText,
pickerRootVisible,
pickerExcludeCurrentDocument: true,
pickerSearchQuery: keyboardQuery,
pickerSearchTargetId: fixture.targetId,
};
} finally {
await page.close().catch(() => undefined);
}
}
async function runStreamFallbackCheck(context, fixture) {
2026-04-29 12:24:44 +08:00
const response = await context.request.fetch(
`${BASE_URL}/api/tree/events?workspaceId=${encodeURIComponent(fixture.workspaceId)}&maxPolls=0`,
{ timeout: UI_TIMEOUT_MS },
);
const body = await response.text();
assert(response.ok(), `/api/tree/events 请求失败: ${response.status()} ${body.slice(0, 200)}`);
assert(
(response.headers()["content-type"] || "").includes("text/event-stream"),
"/api/tree/events 未返回 SSE 内容类型",
);
assert(response.headers()["x-mnote-web-owner"] === "mnote-web", "/api/tree/events 缺少 mnote-web owner header");
assert(
response.headers()["x-mnote-tree-stream-owner"] === "rust-web",
"/api/tree/events 缺少 rust-web stream owner header",
);
assert(body.includes("event: snapshot") || body.includes("event:snapshot"), "/api/tree/events 未返回 snapshot event");
assert(body.includes('"kind":"snapshot"'), "/api/tree/events snapshot payload 缺少 kind=snapshot");
2026-04-26 04:29:23 +08:00
2026-04-29 12:24:44 +08:00
return {
streamFallback: false,
rustTreeEvents: true,
streamRequestCount: 1,
};
2026-04-26 04:29:23 +08:00
}
async function main() {
const headless =
process.env.MNOTE_SMOKE_HEADLESS === "1"
? true
: process.env.MNOTE_SMOKE_HEADLESS === "0"
? false
: !process.env.DISPLAY;
2026-04-29 12:24:44 +08:00
const browser = await chromium.launch({ headless, args: CHROMIUM_STABLE_ARGS });
const browserContextOptions = {
2026-04-26 04:29:23 +08:00
viewport: { width: 1440, height: 960 },
2026-04-29 12:24:44 +08:00
};
let context = await browser.newContext(browserContextOptions);
let page = await context.newPage();
2026-04-26 04:29:23 +08:00
let fixture = null;
let caughtError = null;
try {
const viewer = await ensureAuthenticated(page, context.request);
fixture = await prepareFixture(context.request);
const pageTreeHost = await runPageTreeHostChecks(page, fixture);
2026-04-29 12:24:44 +08:00
const storageState = await context.storageState();
await page.close().catch(() => undefined);
await context.close().catch(() => undefined);
context = await browser.newContext({
...browserContextOptions,
storageState,
});
page = null;
2026-04-26 04:29:23 +08:00
const pageTreeShell = RUN_DIRECT_TREE_SHELL_CHECKS
? await runPageTreeShellChecks(context, fixture)
: { skipped: true, reason: "direct_tree_shell_debug_disabled" };
const fileTreeShell = RUN_DIRECT_TREE_SHELL_CHECKS
? await runFileTreeShellChecks(context, fixture)
: { skipped: true, reason: "direct_tree_shell_debug_disabled" };
2026-04-29 12:24:44 +08:00
let fileTreeHost;
try {
fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
} catch (error) {
if (!isPlaywrightTargetCrash(error)) {
throw error;
}
await context.close().catch(() => undefined);
context = await browser.newContext({
...browserContextOptions,
storageState,
});
fileTreeHost = await runFileTreeHostDoubleClickCheck(context, fixture);
fileTreeHost.recoveredFromRendererCrash = true;
}
2026-04-26 04:29:23 +08:00
const picker = await runPickerDialogChecks(context, fixture);
const streamFallback = await runStreamFallbackCheck(context, fixture);
console.log(
JSON.stringify(
{
ok: true,
baseUrl: BASE_URL,
2026-04-29 12:24:44 +08:00
documentBaseUrl: DOCUMENT_UI_BASE_URL,
2026-04-26 04:29:23 +08:00
viewerUserId: viewer.userId,
workspaceId: fixture.workspaceId,
parentId: fixture.parentId,
childAId: fixture.childAId,
childBId: fixture.childBId,
targetId: fixture.targetId,
results: {
pageTreeHost,
pageTreeShell,
fileTreeShell,
fileTreeHost,
picker,
streamFallback,
},
},
null,
2,
),
);
} catch (error) {
caughtError = error;
} finally {
if (fixture) {
try {
await cleanupFixture(context.request, fixture);
} catch (cleanupError) {
if (!caughtError) {
caughtError = cleanupError;
} else {
console.error(
`清理临时页面失败:${
cleanupError instanceof Error
? cleanupError.stack || cleanupError.message
: String(cleanupError)
}`,
);
}
}
}
2026-04-29 12:24:44 +08:00
if (page) {
await page.close().catch(() => undefined);
}
2026-04-26 04:29:23 +08:00
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
if (caughtError) {
throw caughtError;
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});