20260513 mindmap优化01
This commit is contained in:
@@ -179,6 +179,15 @@ async function waitForTextAnywhere(page, expectedText) {
|
||||
|
||||
async function getPageTreeHostDriver(page) {
|
||||
const host = page.getByTestId("sidebar-page-tree-shell");
|
||||
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") };
|
||||
}
|
||||
await host.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
@@ -222,6 +231,15 @@ async function getPageTreeHostDriver(page) {
|
||||
|
||||
async function getFileTreeHostDriver(page) {
|
||||
const host = page.getByTestId("sidebar-file-tree-shell");
|
||||
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 };
|
||||
}
|
||||
await host.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
@@ -341,7 +359,12 @@ async function doubleClickFileTreeDocumentFromHost(page, driver, documentId) {
|
||||
}
|
||||
|
||||
async function waitForPageTitleInput(page) {
|
||||
const input = page.getByLabel("页面标题");
|
||||
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();
|
||||
await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
return input;
|
||||
}
|
||||
@@ -923,13 +946,26 @@ async function runPickerDialogChecks(context, fixture) {
|
||||
try {
|
||||
await openDocument(page, fixture.workspaceId, fixture.childAId);
|
||||
await waitForPageTitleInput(page);
|
||||
await ensurePageOptionsVisible(page);
|
||||
await ensurePageOptionsVisible(page).catch(() => undefined);
|
||||
await page.getByRole("button", { name: "页面选项", exact: true }).waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
}).catch(() => undefined);
|
||||
const openButton = page.getByRole("button", { name: "移动/嵌入到..." });
|
||||
await openButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
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",
|
||||
};
|
||||
}
|
||||
const openPickerDialog = async () => {
|
||||
await openButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
const dialog = page.getByRole("dialog");
|
||||
|
||||
@@ -73,6 +73,7 @@ async function readMindmapMetrics(page) {
|
||||
const schemaNavigator = document.querySelector('[data-testid="mindmap-schema-navigator"]');
|
||||
const schemaCount = document.querySelector('[data-testid="mindmap-schema-count"]');
|
||||
const schemaContextMenu = document.querySelector('[data-testid="mindmap-schema-context-menu"]');
|
||||
const schemaZoomInput = document.querySelector('[data-testid="mindmap-schema-navigator-zoom-input"]');
|
||||
const rustShellMount = document.querySelector('[data-testid="mindmap-rust-shell-mount"]');
|
||||
const rustShell = document.querySelector('[data-testid="mindmap-rust-shell"]');
|
||||
const error = document.querySelector('[data-testid="leptos-mindmap-error"]');
|
||||
@@ -162,6 +163,7 @@ async function readMindmapMetrics(page) {
|
||||
statsText: schemaNavigator?.querySelector('[data-testid="mindmap-schema-navigator-stats"]')?.textContent || null,
|
||||
countText: schemaCount?.textContent || null,
|
||||
zoomText: schemaNavigator?.querySelector('[data-testid="mindmap-schema-navigator-zoom"]')?.textContent || null,
|
||||
zoomValue: schemaZoomInput instanceof HTMLInputElement ? schemaZoomInput.value : null,
|
||||
minimap: Boolean(document.querySelector('[data-testid="mindmap-schema-minimap"]')),
|
||||
contextMenu:
|
||||
schemaContextMenu instanceof HTMLElement
|
||||
@@ -193,6 +195,12 @@ async function readMindmapMetrics(page) {
|
||||
},
|
||||
viewCommandStatus:
|
||||
scene instanceof HTMLElement ? scene.dataset.viewCommandStatus || null : null,
|
||||
runtimeMountCount:
|
||||
scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0,
|
||||
lastRuntimeMountReason:
|
||||
scene instanceof HTMLElement ? scene.dataset.lastRuntimeMountReason || null : null,
|
||||
lastFetchSceneReason:
|
||||
scene instanceof HTMLElement ? scene.dataset.lastFetchSceneReason || null : null,
|
||||
bridgeExists: Boolean(bridge),
|
||||
runtimeBox: runtimeRect
|
||||
? { width: runtimeRect.width, height: runtimeRect.height }
|
||||
@@ -284,7 +292,7 @@ async function assertMindmapReady(page, stage) {
|
||||
assert(metrics.schemaChrome.sidebarPanels.includes(panelId), `${stage}: ui_shell_missing panel=${panelId}`);
|
||||
}
|
||||
assert(/节点/.test(metrics.schemaChrome.countText || ""), `${stage}: ui_shell_missing count_stats`);
|
||||
assert(/%/.test(metrics.schemaChrome.zoomText || ""), `${stage}: ui_shell_missing zoom`);
|
||||
assert(/%/.test(metrics.schemaChrome.zoomText || metrics.schemaChrome.zoomValue || ""), `${stage}: ui_shell_missing zoom`);
|
||||
assert(metrics.nodeCount >= 4, `${stage}: runtime_render_failed nodeCount=${metrics.nodeCount}`);
|
||||
assert(metrics.edgeCount >= 3, `${stage}: runtime_render_failed edgeCount=${metrics.edgeCount}`);
|
||||
return metrics;
|
||||
@@ -314,7 +322,47 @@ async function readMindmapSnapshotStats(page, mindmapId) {
|
||||
}, mindmapId);
|
||||
}
|
||||
|
||||
async function clickToolbarActionAndWaitForCommand(page, actionId) {
|
||||
function findNodeFillColorByUid(root, nodeId) {
|
||||
if (!root || typeof root !== "object" || Array.isArray(root) || !nodeId) return null;
|
||||
const queue = [root];
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift();
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) continue;
|
||||
const uid = typeof node.data?.uid === "string" ? node.data.uid.trim() : "";
|
||||
if (uid === nodeId) {
|
||||
return typeof node.data?.fillColor === "string" ? node.data.fillColor : null;
|
||||
}
|
||||
if (Array.isArray(node.children)) queue.push(...node.children);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function armBridgeStabilityProbe(page, mindmapId, probeKey) {
|
||||
await page.evaluate(
|
||||
({ mindmapId, probeKey }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
window.__mnoteSmokeBridgeProbes = window.__mnoteSmokeBridgeProbes || {};
|
||||
window.__mnoteSmokeBridgeProbes[probeKey] = registry[mindmapId] || null;
|
||||
},
|
||||
{ mindmapId, probeKey },
|
||||
);
|
||||
}
|
||||
|
||||
async function readBridgeStabilityProbe(page, mindmapId, probeKey) {
|
||||
return page.evaluate(
|
||||
({ mindmapId, probeKey }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const probes = window.__mnoteSmokeBridgeProbes || {};
|
||||
return Boolean(probes[probeKey] && registry[mindmapId] === probes[probeKey]);
|
||||
},
|
||||
{ mindmapId, probeKey },
|
||||
);
|
||||
}
|
||||
|
||||
async function clickToolbarActionAndWaitForCommand(page, mindmapId, actionId) {
|
||||
const probeKey = `toolbar:${actionId}:${Date.now()}`;
|
||||
await armBridgeStabilityProbe(page, mindmapId, probeKey);
|
||||
const beforeMetrics = await readMindmapMetrics(page);
|
||||
await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
@@ -349,7 +397,13 @@ async function clickToolbarActionAndWaitForCommand(page, actionId) {
|
||||
}, actionId);
|
||||
throw new Error(`command_failed:${actionId}:command_status_timeout:${JSON.stringify(diagnostics)}:${error.message}`);
|
||||
});
|
||||
return { ok: () => true };
|
||||
const bridgeStable = await readBridgeStabilityProbe(page, mindmapId, probeKey);
|
||||
const afterMetrics = await readMindmapMetrics(page);
|
||||
const afterSceneDataset = await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement ? { ...scene.dataset } : null;
|
||||
});
|
||||
return { ok: () => true, bridgeStable, beforeMetrics, afterMetrics, afterSceneDataset };
|
||||
}
|
||||
|
||||
async function clickSidebarOptionAndWaitForCommand(page, optionId, actionId) {
|
||||
@@ -427,7 +481,7 @@ async function verifyReadonlyToolbarState(page, stage) {
|
||||
|
||||
async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
|
||||
const before = await readMindmapSnapshotStats(page, mindmapId);
|
||||
const childResponse = await clickToolbarActionAndWaitForCommand(page, "insertChild");
|
||||
const childResponse = await clickToolbarActionAndWaitForCommand(page, mindmapId, "insertChild");
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, expected }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
@@ -451,7 +505,7 @@ async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
|
||||
);
|
||||
const afterChild = await readMindmapSnapshotStats(page, mindmapId);
|
||||
|
||||
const siblingResponse = await clickToolbarActionAndWaitForCommand(page, "insertSiblingAfter");
|
||||
const siblingResponse = await clickToolbarActionAndWaitForCommand(page, mindmapId, "insertSiblingAfter");
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, expected }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
@@ -475,7 +529,7 @@ async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
|
||||
);
|
||||
const afterSibling = await readMindmapSnapshotStats(page, mindmapId);
|
||||
|
||||
const deleteResponse = await clickToolbarActionAndWaitForCommand(page, "deleteNode");
|
||||
const deleteResponse = await clickToolbarActionAndWaitForCommand(page, mindmapId, "deleteNode");
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, expectedMax }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
@@ -503,6 +557,18 @@ async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
|
||||
insertChildOk: childResponse.ok() && afterChild.nodeCount >= before.nodeCount + 1,
|
||||
insertSiblingOk: siblingResponse.ok() && afterSibling.nodeCount >= afterChild.nodeCount + 1,
|
||||
deleteOk: deleteResponse.ok() && afterDelete.nodeCount <= afterSibling.nodeCount - 1,
|
||||
childBridgeStable: childResponse.bridgeStable,
|
||||
siblingBridgeStable: siblingResponse.bridgeStable,
|
||||
deleteBridgeStable: deleteResponse.bridgeStable,
|
||||
childRuntimeMountCountBefore: childResponse.beforeMetrics.runtimeMountCount,
|
||||
childRuntimeMountCountAfter: childResponse.afterMetrics.runtimeMountCount,
|
||||
childRuntimeMountReason: childResponse.afterMetrics.lastRuntimeMountReason,
|
||||
childFetchSceneReason: childResponse.afterMetrics.lastFetchSceneReason,
|
||||
childCommandApplyMode: childResponse.afterSceneDataset?.commandApplyMode ?? null,
|
||||
childCommandRuntimeError: childResponse.afterSceneDataset?.commandRuntimeError ?? null,
|
||||
childCommandRuntimeMessage: childResponse.afterSceneDataset?.commandRuntimeMessage ?? null,
|
||||
childCommandActiveNodeId: childResponse.afterSceneDataset?.lastSchemaActiveNodeId ?? null,
|
||||
childCommandLocalApplyErrors: childResponse.afterSceneDataset?.commandLocalApplyErrors ?? null,
|
||||
beforeNodeCount: before.nodeCount,
|
||||
afterChildNodeCount: afterChild.nodeCount,
|
||||
afterSiblingNodeCount: afterSibling.nodeCount,
|
||||
@@ -510,6 +576,120 @@ async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
|
||||
};
|
||||
}
|
||||
|
||||
async function pressMindmapShortcutAndWaitForCommand(page, mindmapId, key, actionId) {
|
||||
const probeKey = `shortcut:${actionId}:${Date.now()}`;
|
||||
await armBridgeStabilityProbe(page, mindmapId, probeKey);
|
||||
const beforeMetrics = await readMindmapMetrics(page);
|
||||
const activated = await page.evaluate((mindmapId) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const renderer = bridge?.instance?.renderer;
|
||||
if (!renderer) return false;
|
||||
const snapshot = bridge?.getSnapshot?.();
|
||||
const rootData =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
const queue = Array.isArray(rootData?.children) ? [...rootData.children] : [];
|
||||
let targetUid = null;
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift();
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) continue;
|
||||
const uid = typeof node.data?.uid === "string" ? node.data.uid.trim() : "";
|
||||
if (uid) {
|
||||
targetUid = uid;
|
||||
break;
|
||||
}
|
||||
if (Array.isArray(node.children)) queue.push(...node.children);
|
||||
}
|
||||
const target =
|
||||
(targetUid && typeof renderer.findNodeByUid === "function" ? renderer.findNodeByUid(targetUid) : null) ??
|
||||
renderer.activeNodeList?.find?.((node) => node && node.isRoot !== true && node.isGeneralization !== true) ??
|
||||
renderer.lastActiveNodeList?.find?.((node) => node && node.isRoot !== true && node.isGeneralization !== true) ??
|
||||
null;
|
||||
if (!target) return false;
|
||||
renderer.clearActiveNodeList?.();
|
||||
renderer.addNodeToActiveList?.(target, true);
|
||||
renderer.lastActiveNodeList = [target];
|
||||
renderer.emitNodeActiveEvent?.(target, [target]);
|
||||
bridge.instance?.execCommand?.("SET_NODE_ACTIVE", target, true);
|
||||
return true;
|
||||
}, mindmapId);
|
||||
if (!activated) {
|
||||
throw new Error(`shortcut_failed:${actionId}:${key}:non_root_activation_failed`);
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
scene.dataset.commandStatus = "smoke-waiting";
|
||||
delete scene.dataset.lastSchemaAction;
|
||||
delete scene.dataset.lastShortcutAction;
|
||||
delete scene.dataset.lastShortcutActionBlocked;
|
||||
}
|
||||
});
|
||||
await page.keyboard.press(key);
|
||||
await page.waitForFunction(
|
||||
({ actionId }) => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return (
|
||||
scene instanceof HTMLElement &&
|
||||
scene.dataset.lastShortcutAction === actionId &&
|
||||
scene.dataset.commandStatus === "success"
|
||||
);
|
||||
},
|
||||
{ actionId },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
).catch(async (error) => {
|
||||
const diagnostics = await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
|
||||
return {
|
||||
sceneDataset: scene instanceof HTMLElement ? { ...scene.dataset } : null,
|
||||
rootExists: root instanceof HTMLElement,
|
||||
};
|
||||
});
|
||||
throw new Error(`shortcut_failed:${actionId}:${key}:${JSON.stringify(diagnostics)}:${error.message}`);
|
||||
});
|
||||
const bridgeStable = await readBridgeStabilityProbe(page, mindmapId, probeKey);
|
||||
const afterMetrics = await readMindmapMetrics(page);
|
||||
const afterSceneDataset = await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement ? { ...scene.dataset } : null;
|
||||
});
|
||||
return { ok: true, bridgeStable, beforeMetrics, afterMetrics, afterSceneDataset };
|
||||
}
|
||||
|
||||
async function exerciseKeyboardShortcuts(page, mindmapId) {
|
||||
const before = await readMindmapSnapshotStats(page, mindmapId);
|
||||
const enterResponse = await pressMindmapShortcutAndWaitForCommand(page, mindmapId, "Enter", "insertSiblingAfter");
|
||||
const afterEnter = await readMindmapSnapshotStats(page, mindmapId);
|
||||
const deleteResponse = await pressMindmapShortcutAndWaitForCommand(page, mindmapId, "Delete", "deleteNode");
|
||||
const afterDelete = await readMindmapSnapshotStats(page, mindmapId);
|
||||
const rootStillExists = await page.evaluate(() => {
|
||||
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
|
||||
return root instanceof HTMLElement;
|
||||
});
|
||||
return {
|
||||
enterOk: enterResponse.ok && afterEnter.nodeCount >= before.nodeCount + 1,
|
||||
deleteOk: deleteResponse.ok && afterDelete.nodeCount <= afterEnter.nodeCount - 1,
|
||||
enterBridgeStable: enterResponse.bridgeStable,
|
||||
deleteBridgeStable: deleteResponse.bridgeStable,
|
||||
enterRuntimeMountCountBefore: enterResponse.beforeMetrics.runtimeMountCount,
|
||||
enterRuntimeMountCountAfter: enterResponse.afterMetrics.runtimeMountCount,
|
||||
enterRuntimeMountReason: enterResponse.afterMetrics.lastRuntimeMountReason,
|
||||
enterFetchSceneReason: enterResponse.afterMetrics.lastFetchSceneReason,
|
||||
enterCommandApplyMode: enterResponse.afterSceneDataset?.commandApplyMode ?? null,
|
||||
enterCommandRuntimeError: enterResponse.afterSceneDataset?.commandRuntimeError ?? null,
|
||||
enterCommandRuntimeMessage: enterResponse.afterSceneDataset?.commandRuntimeMessage ?? null,
|
||||
enterCommandActiveNodeId: enterResponse.afterSceneDataset?.lastSchemaActiveNodeId ?? null,
|
||||
enterCommandLocalApplyErrors: enterResponse.afterSceneDataset?.commandLocalApplyErrors ?? null,
|
||||
rootStillExists,
|
||||
beforeNodeCount: before.nodeCount,
|
||||
afterEnterNodeCount: afterEnter.nodeCount,
|
||||
afterDeleteNodeCount: afterDelete.nodeCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function exerciseSidebarActions(page, documentId, mindmapId) {
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-theme").click({ timeout: UI_TIMEOUT_MS });
|
||||
await clickSidebarOptionAndWaitForCommand(page, "theme-classic4", "setTheme");
|
||||
@@ -528,9 +708,27 @@ async function exerciseSidebarActions(page, documentId, mindmapId) {
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-nodeStyle").click({ timeout: UI_TIMEOUT_MS });
|
||||
await clickSidebarOptionAndWaitForCommand(page, "node-fill-blue", "painter");
|
||||
const nodeStyleProjection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
const rootFill = nodeStyleProjection?.root?.data?.fillColor;
|
||||
if (rootFill !== "#dbeafe") {
|
||||
throw new Error(`command_failed:nodeStyle compat fill=${JSON.stringify(rootFill)}`);
|
||||
const nodeStyleDiagnostics = await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement ? { ...scene.dataset } : null;
|
||||
});
|
||||
const filledNodes = [];
|
||||
const collectFilledNodes = (node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
||||
const uid = typeof node.data?.uid === "string" ? node.data.uid : null;
|
||||
const fillColor = typeof node.data?.fillColor === "string" ? node.data.fillColor : null;
|
||||
if (uid && fillColor) filledNodes.push({ uid, fillColor });
|
||||
if (Array.isArray(node.children)) node.children.forEach(collectFilledNodes);
|
||||
};
|
||||
collectFilledNodes(nodeStyleProjection?.root);
|
||||
const activeNodeId = typeof nodeStyleDiagnostics?.lastSchemaActiveNodeId === "string"
|
||||
? nodeStyleDiagnostics.lastSchemaActiveNodeId
|
||||
: null;
|
||||
const activeNodeFill = activeNodeId
|
||||
? filledNodes.find((node) => node.uid === activeNodeId)?.fillColor ?? null
|
||||
: null;
|
||||
if (activeNodeFill !== "#dbeafe") {
|
||||
throw new Error(`command_failed:nodeStyle compat fill=${JSON.stringify(activeNodeFill)} active=${JSON.stringify(activeNodeId)} nodes=${JSON.stringify(filledNodes)}`);
|
||||
}
|
||||
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-baseStyle").click({ timeout: UI_TIMEOUT_MS });
|
||||
@@ -552,7 +750,8 @@ async function exerciseSidebarActions(page, documentId, mindmapId) {
|
||||
return {
|
||||
themeOk: themedProjection.theme === "classic4",
|
||||
layoutOk: layoutProjection.layout === "mindMap",
|
||||
nodeStyleCompatOk: rootFill === "#dbeafe",
|
||||
nodeStyleCompatOk: activeNodeFill === "#dbeafe",
|
||||
nodeStyleTargetNodeId: activeNodeId,
|
||||
baseStyleCompatOk: lineStyle === "curve",
|
||||
outlineDerivedOk: outlineItems.length > 0,
|
||||
outlineItems,
|
||||
@@ -692,6 +891,8 @@ async function centerRootAndVerifyViewPatch(page, documentId, mindmapId) {
|
||||
async function exerciseNavigatorActions(page, documentId, mindmapId) {
|
||||
const centerRootOk = await centerRootAndVerifyViewPatch(page, documentId, mindmapId);
|
||||
const zoomScale = await zoomMindmapAndVerifyViewPatch(page, documentId, mindmapId);
|
||||
await page.getByTestId("mindmap-schema-navigator-action-search").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.getByTestId("mindmap-schema-navigator-search").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.getByTestId("mindmap-schema-navigator-search").fill("KMIND", { timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
@@ -829,6 +1030,7 @@ async function main() {
|
||||
viewScale: null,
|
||||
navigatorActions: null,
|
||||
contextMenuActions: null,
|
||||
keyboardActions: null,
|
||||
screenshots,
|
||||
ports: portReport,
|
||||
};
|
||||
@@ -870,8 +1072,25 @@ async function main() {
|
||||
assert(toolbarActions.insertChildOk, "command_failed:insertChild");
|
||||
assert(toolbarActions.insertSiblingOk, "command_failed:insertSiblingAfter");
|
||||
assert(toolbarActions.deleteOk, "command_failed:deleteNode");
|
||||
assert(
|
||||
toolbarActions.childBridgeStable,
|
||||
`runtime_remount_detected:insertChild mounts=${toolbarActions.childRuntimeMountCountBefore}->${toolbarActions.childRuntimeMountCountAfter} mountReason=${toolbarActions.childRuntimeMountReason} fetchReason=${toolbarActions.childFetchSceneReason} activeNode=${toolbarActions.childCommandActiveNodeId} applyMode=${toolbarActions.childCommandApplyMode} runtimeError=${toolbarActions.childCommandRuntimeError} runtimeMessage=${toolbarActions.childCommandRuntimeMessage} localErrors=${toolbarActions.childCommandLocalApplyErrors}`,
|
||||
);
|
||||
assert(toolbarActions.siblingBridgeStable, "runtime_remount_detected:insertSiblingAfter");
|
||||
assert(toolbarActions.deleteBridgeStable, "runtime_remount_detected:deleteNode");
|
||||
screenshots.push(await screenshot(page, "03-after-toolbar-actions"));
|
||||
|
||||
const keyboardActions = await exerciseKeyboardShortcuts(page, mindmapId);
|
||||
assert(keyboardActions.enterOk, "shortcut_failed:insertSiblingAfter");
|
||||
assert(keyboardActions.deleteOk, "shortcut_failed:deleteNode");
|
||||
assert(
|
||||
keyboardActions.enterBridgeStable,
|
||||
`runtime_remount_detected:shortcut_enter mounts=${keyboardActions.enterRuntimeMountCountBefore}->${keyboardActions.enterRuntimeMountCountAfter} mountReason=${keyboardActions.enterRuntimeMountReason} fetchReason=${keyboardActions.enterFetchSceneReason} activeNode=${keyboardActions.enterCommandActiveNodeId} applyMode=${keyboardActions.enterCommandApplyMode} runtimeError=${keyboardActions.enterCommandRuntimeError} runtimeMessage=${keyboardActions.enterCommandRuntimeMessage} localErrors=${keyboardActions.enterCommandLocalApplyErrors}`,
|
||||
);
|
||||
assert(keyboardActions.deleteBridgeStable, "runtime_remount_detected:shortcut_delete");
|
||||
assert(keyboardActions.rootStillExists, "shortcut_failed:mindmap_root_deleted");
|
||||
screenshots.push(await screenshot(page, "03b-after-keyboard-actions"));
|
||||
|
||||
for (const [panelId, name] of [
|
||||
["nodeStyle", "sidebar-node-style"],
|
||||
["baseStyle", "sidebar-base-style"],
|
||||
@@ -917,9 +1136,10 @@ async function main() {
|
||||
const reloadProjection = await fetchAdapterProjection(page, doc.documentId, mindmapId);
|
||||
assert(reloadProjection?.theme === "classic4", `reload_mismatch:theme actual=${JSON.stringify(reloadProjection?.theme)}`);
|
||||
assert(reloadProjection?.layout === "mindMap", `reload_mismatch:layout actual=${JSON.stringify(reloadProjection?.layout)}`);
|
||||
const reloadNodeStyleFill = findNodeFillColorByUid(reloadProjection?.root, sidebarActions.nodeStyleTargetNodeId);
|
||||
assert(
|
||||
reloadProjection?.root?.data?.fillColor === "#dbeafe",
|
||||
`reload_mismatch:node_style actual=${JSON.stringify(reloadProjection?.root?.data)}`,
|
||||
reloadNodeStyleFill === "#dbeafe",
|
||||
`reload_mismatch:node_style active=${JSON.stringify(sidebarActions.nodeStyleTargetNodeId)} actual=${JSON.stringify(reloadProjection?.root?.data)}`,
|
||||
);
|
||||
assert(
|
||||
reloadProjection?.compatPayload?.style?.map?.lineStyle === "curve",
|
||||
@@ -933,6 +1153,7 @@ async function main() {
|
||||
nodeCount: reloaded.nodeCount,
|
||||
edgeCount: reloaded.edgeCount,
|
||||
toolbarActions,
|
||||
keyboardActions,
|
||||
sidebarActions,
|
||||
navigatorActions,
|
||||
contextMenuActions,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
assert,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
openDocument,
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
const TASK = "task168-mindmap-put-validator-smoke";
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const BAD_TEXT_PATTERN = /ArgumentValidationError|domainEventHint|domainEventPlan|streamDeltaHint|command_failed|502 Bad Gateway/i;
|
||||
|
||||
async function writeResult(payload) {
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...payload, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function screenshot(page, name) {
|
||||
const file = path.join(OUTPUT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: file, fullPage: true });
|
||||
return file;
|
||||
}
|
||||
|
||||
function attachMindmapNetworkCapture(page, label, records) {
|
||||
page.on("response", async (response) => {
|
||||
const url = response.url();
|
||||
if (!url.includes("/api/mindmap/") && !url.includes("/mindmap/") && !url.includes("/api/documents/save")) {
|
||||
return;
|
||||
}
|
||||
const request = response.request();
|
||||
const method = request.method();
|
||||
const status = response.status();
|
||||
let responseText = null;
|
||||
if (method !== "GET" || status >= 400) {
|
||||
responseText = await response.text().catch((error) => `<<read_response_failed:${error.message}>>`);
|
||||
}
|
||||
records.push({
|
||||
type: "response",
|
||||
label,
|
||||
method,
|
||||
url,
|
||||
status,
|
||||
statusText: response.statusText(),
|
||||
requestBody: request.postData() || null,
|
||||
responseText: responseText ? responseText.slice(0, 3000) : null,
|
||||
});
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
const url = request.url();
|
||||
if (url.includes("/api/mindmap/") || url.includes("/mindmap/") || url.includes("/api/documents/save")) {
|
||||
records.push({
|
||||
type: "requestfailed",
|
||||
label,
|
||||
method: request.method(),
|
||||
url,
|
||||
failure: request.failure()?.errorText || null,
|
||||
requestBody: request.postData() || null,
|
||||
});
|
||||
}
|
||||
});
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
const text = message.text();
|
||||
if (/mindmap|ArgumentValidationError|domainEvent|502|command_failed/i.test(text)) {
|
||||
records.push({
|
||||
type: "console",
|
||||
label,
|
||||
level: message.type(),
|
||||
text: text.slice(0, 2000),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function insertMindmapThroughSlash(page) {
|
||||
const editor = page
|
||||
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
||||
.first();
|
||||
await editor.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await editor.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.type("/");
|
||||
const item = page.getByTestId("slash-item-mindmap").first();
|
||||
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await item.click({ timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
async function readMindmapState(page) {
|
||||
return page.evaluate(() => {
|
||||
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
|
||||
const error = document.querySelector('[data-testid="leptos-mindmap-error"]');
|
||||
return {
|
||||
mindmapId: root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null,
|
||||
runtimeReady: runtime instanceof HTMLElement ? runtime.dataset.runtimeReady === "true" : false,
|
||||
commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || null : null,
|
||||
lastSchemaAction: scene instanceof HTMLElement ? scene.dataset.lastSchemaAction || null : null,
|
||||
commandRuntimeError: scene instanceof HTMLElement ? scene.dataset.commandRuntimeError || null : null,
|
||||
commandRuntimeMessage: scene instanceof HTMLElement ? scene.dataset.commandRuntimeMessage || null : null,
|
||||
errorStage: error instanceof HTMLElement ? error.dataset.stage || null : null,
|
||||
errorText: error instanceof HTMLElement ? (error.textContent || "").slice(0, 3000) : null,
|
||||
bodyText: (document.body?.innerText || "").slice(0, 5000),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function assertNoValidatorLeak(page, records, stage) {
|
||||
await page.waitForTimeout(500);
|
||||
const state = await readMindmapState(page);
|
||||
const badRecord = records.find((record) => {
|
||||
if (record.type === "response" && record.status >= 500) return true;
|
||||
return BAD_TEXT_PATTERN.test(`${record.responseText || ""}\n${record.text || ""}\n${record.failure || ""}`);
|
||||
});
|
||||
if (badRecord || BAD_TEXT_PATTERN.test(`${state.errorText || ""}\n${state.bodyText || ""}`)) {
|
||||
throw new Error(
|
||||
`${stage}:mindmap_validator_or_502_leak:${JSON.stringify(
|
||||
{
|
||||
state,
|
||||
badRecord,
|
||||
recentMindmapNetwork: records.slice(-12),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
async function waitForMindmapReady(page, stage) {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
|
||||
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
|
||||
const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null;
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
return (
|
||||
root instanceof HTMLElement &&
|
||||
runtime instanceof HTMLElement &&
|
||||
runtime.dataset.runtimeReady === "true" &&
|
||||
Boolean(mindmapId) &&
|
||||
Boolean(registry[mindmapId])
|
||||
);
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
)
|
||||
.catch((error) => {
|
||||
throw new Error(`${stage}:mindmap_not_ready:${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForDocumentContentToIncludeMindmap(requestContext, documentId, workspaceId, mindmapId) {
|
||||
let lastText = "";
|
||||
for (let index = 0; index < 45; index += 1) {
|
||||
const response = await requestContext.fetch(
|
||||
`${BASE_URL}/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
|
||||
{ method: "GET", timeout: 10_000 },
|
||||
);
|
||||
lastText = await response.text();
|
||||
if (response.ok() && lastText.includes(mindmapId)) {
|
||||
try {
|
||||
return JSON.parse(lastText);
|
||||
} catch {
|
||||
return { raw: lastText.slice(0, 3000) };
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(
|
||||
`document_content_missing_mindmap:${JSON.stringify({
|
||||
documentId,
|
||||
workspaceId,
|
||||
mindmapId,
|
||||
lastText: lastText.slice(0, 5000),
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchMindmapProjection(requestContext, documentId, mindmapId) {
|
||||
const response = await requestContext.fetch(
|
||||
`${BASE_URL}/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`,
|
||||
{ method: "GET", timeout: 10_000 },
|
||||
);
|
||||
const text = await response.text();
|
||||
assert(response.ok(), `mindmap_projection_fetch_failed:${response.status()}:${text.slice(0, 2000)}`);
|
||||
const payload = JSON.parse(text);
|
||||
return payload.result ?? payload;
|
||||
}
|
||||
|
||||
function collectMindmapTexts(root) {
|
||||
const texts = [];
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
const data = node.data && typeof node.data === "object" ? node.data : {};
|
||||
if (typeof data.text === "string") texts.push(data.text);
|
||||
if (Array.isArray(node.children)) node.children.forEach(visit);
|
||||
};
|
||||
visit(root);
|
||||
return texts;
|
||||
}
|
||||
|
||||
async function waitForProjectionText(requestContext, documentId, mindmapId, expectedText) {
|
||||
let lastProjection = null;
|
||||
for (let index = 0; index < 45; index += 1) {
|
||||
lastProjection = await fetchMindmapProjection(requestContext, documentId, mindmapId);
|
||||
const texts = collectMindmapTexts(lastProjection.root);
|
||||
if (texts.includes(expectedText)) {
|
||||
return { projection: lastProjection, texts };
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(
|
||||
`mindmap_projection_missing_edited_text:${JSON.stringify({
|
||||
expectedText,
|
||||
texts: collectMindmapTexts(lastProjection?.root),
|
||||
projection: lastProjection,
|
||||
}).slice(0, 5000)}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function editTopicTextThroughRuntime(page, mindmapId, text) {
|
||||
const result = await page.evaluate(
|
||||
({ mindmapId, text }) => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
scene.dataset.lastDataChangeRefreshTriggered = "";
|
||||
scene.dataset.lastDataChangeDiffCommandCount = "";
|
||||
scene.dataset.commandStatus = "smoke-waiting-topic-edit";
|
||||
}
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const instance = bridge?.instance;
|
||||
const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null;
|
||||
if (!topicNode || typeof topicNode !== "object") {
|
||||
return { ok: false, reason: "topic_node_missing" };
|
||||
}
|
||||
if (typeof topicNode.setData === "function") {
|
||||
topicNode.setData({ text });
|
||||
} else if (topicNode.data && typeof topicNode.data === "object") {
|
||||
topicNode.data.text = text;
|
||||
} else {
|
||||
return { ok: false, reason: "topic_node_not_mutable" };
|
||||
}
|
||||
const snapshot = typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : instance?.getData?.(true);
|
||||
return {
|
||||
ok: true,
|
||||
snapshotText: snapshot?.children?.[0]?.data?.text ?? null,
|
||||
commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || null : null,
|
||||
diffCommandCount: scene instanceof HTMLElement ? scene.dataset.lastDataChangeDiffCommandCount || null : null,
|
||||
};
|
||||
},
|
||||
{ mindmapId, text },
|
||||
);
|
||||
assert(result.ok, `topic_runtime_edit_failed:${JSON.stringify(result)}`);
|
||||
await page.waitForFunction(
|
||||
({ text }) => {
|
||||
const bodyText = document.body?.innerText || "";
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return (
|
||||
bodyText.includes(text) ||
|
||||
(scene instanceof HTMLElement && scene.dataset.commandStatus !== "smoke-waiting-topic-edit")
|
||||
);
|
||||
},
|
||||
{ text },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function clickInsertChild(page) {
|
||||
const button = page.getByTestId("mindmap-schema-toolbar-action-insertChild");
|
||||
await button.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const disabled = await button.evaluate((node) => node instanceof HTMLButtonElement && node.disabled);
|
||||
if (disabled) {
|
||||
await page.locator('[data-testid="simple-mind-map-runtime"] .smm-node').first().click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(() => {
|
||||
const button = document.querySelector('[data-testid="mindmap-schema-toolbar-action-insertChild"]');
|
||||
return button instanceof HTMLButtonElement && !button.disabled;
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
scene.dataset.commandStatus = "smoke-waiting";
|
||||
delete scene.dataset.lastSchemaAction;
|
||||
}
|
||||
});
|
||||
await button.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement && scene.dataset.lastSchemaAction === "insertChild" && scene.dataset.commandStatus === "success";
|
||||
}, null, { timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const contextA = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
|
||||
const pageA = await contextA.newPage();
|
||||
const records = [];
|
||||
attachMindmapNetworkCapture(pageA, "browser-a", records);
|
||||
const screenshots = [];
|
||||
const createdIds = [];
|
||||
const result = {
|
||||
ok: false,
|
||||
task: TASK,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: null,
|
||||
workspaceId: null,
|
||||
mindmapId: null,
|
||||
network: records,
|
||||
screenshots,
|
||||
};
|
||||
|
||||
let contextB = null;
|
||||
let pageB = null;
|
||||
try {
|
||||
await ensureAuthenticated(pageA, contextA.request);
|
||||
const doc = await createTempDocument(contextA.request, null);
|
||||
createdIds.push(doc.documentId);
|
||||
result.documentId = doc.documentId;
|
||||
result.workspaceId = doc.workspaceId;
|
||||
await renameDocument(contextA.request, doc.workspaceId, doc.documentId, `task168-mindmap-${Date.now().toString().slice(-6)}`);
|
||||
|
||||
await openDocument(pageA, doc.workspaceId, doc.documentId);
|
||||
await insertMindmapThroughSlash(pageA);
|
||||
await waitForMindmapReady(pageA, "browser-a-initial");
|
||||
const initialState = await assertNoValidatorLeak(pageA, records, "browser-a-initial");
|
||||
result.mindmapId = initialState.mindmapId;
|
||||
|
||||
await clickInsertChild(pageA);
|
||||
const afterCommand = await assertNoValidatorLeak(pageA, records, "browser-a-insert-child");
|
||||
assert(afterCommand.commandStatus === "success", `insert_child_not_success:${JSON.stringify(afterCommand)}`);
|
||||
screenshots.push(await screenshot(pageA, "01-after-insert-child"));
|
||||
result.documentContentAfterInsert = await waitForDocumentContentToIncludeMindmap(
|
||||
contextA.request,
|
||||
doc.documentId,
|
||||
doc.workspaceId,
|
||||
result.mindmapId,
|
||||
);
|
||||
|
||||
const editedTopicText = `二级节点-SMOKE-${Date.now().toString().slice(-6)}`;
|
||||
result.topicEditAttempt = await editTopicTextThroughRuntime(pageA, result.mindmapId, editedTopicText);
|
||||
const afterTopicEdit = await assertNoValidatorLeak(pageA, records, "browser-a-topic-edit");
|
||||
result.afterTopicEdit = afterTopicEdit;
|
||||
result.topicProjectionAfterEdit = await waitForProjectionText(
|
||||
contextA.request,
|
||||
doc.documentId,
|
||||
result.mindmapId,
|
||||
editedTopicText,
|
||||
);
|
||||
screenshots.push(await screenshot(pageA, "02-after-topic-edit"));
|
||||
|
||||
contextB = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
|
||||
pageB = await contextB.newPage();
|
||||
attachMindmapNetworkCapture(pageB, "browser-b", records);
|
||||
await ensureAuthenticated(pageB, contextB.request);
|
||||
await openDocument(pageB, doc.workspaceId, doc.documentId);
|
||||
await waitForMindmapReady(pageB, "browser-b-reopen");
|
||||
const secondState = await assertNoValidatorLeak(pageB, records, "browser-b-reopen");
|
||||
assert(
|
||||
secondState.bodyText.includes(editedTopicText),
|
||||
`second_browser_missing_edited_topic_text:${JSON.stringify({
|
||||
expected: editedTopicText,
|
||||
secondState,
|
||||
})}`,
|
||||
);
|
||||
assert(
|
||||
secondState.mindmapId === result.mindmapId,
|
||||
`second_browser_mindmap_id_mismatch:${JSON.stringify({ expected: result.mindmapId, actual: secondState.mindmapId })}`,
|
||||
);
|
||||
screenshots.push(await screenshot(pageB, "03-second-browser-visible"));
|
||||
|
||||
result.ok = true;
|
||||
result.initialState = initialState;
|
||||
result.afterCommand = afterCommand;
|
||||
result.secondState = secondState;
|
||||
await writeResult(result);
|
||||
} catch (error) {
|
||||
result.error = error instanceof Error ? error.stack || error.message : String(error);
|
||||
screenshots.push(await screenshot(pageA, "99-failure").catch(() => null));
|
||||
if (pageB) {
|
||||
screenshots.push(await screenshot(pageB, "99-failure-browser-b").catch(() => null));
|
||||
}
|
||||
await writeResult(result);
|
||||
throw error;
|
||||
} finally {
|
||||
if (contextB) {
|
||||
await contextB.close().catch(() => undefined);
|
||||
}
|
||||
await cleanupDocuments(contextA.request, createdIds).catch(() => undefined);
|
||||
await contextA.close().catch(() => undefined);
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user