Files

1200 lines
56 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
"use strict";
const fs = require("node:fs/promises");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
assert,
cleanupDocuments,
createTempDocument,
ensureAuthenticated,
openDocument,
renameDocument,
} = require("./tree-shell-smoke-helpers");
const TASK = "task166-mindmap-phase6-block-smoke";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
function checkPort(port) {
const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
encoding: "utf8",
stdio: "pipe",
});
const lines = (result.stdout || "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const rows = lines.slice(1).map((line) => {
const parts = line.split(/\s+/);
const pid = parts[1] || "";
const commandLine =
pid.length > 0
? spawnSync("ps", ["-p", pid, "-o", "command="], {
encoding: "utf8",
stdio: "pipe",
}).stdout.trim()
: "";
return {
command: parts[0] || "",
pid,
commandLine,
name: parts.slice(8).join(" "),
raw: line,
};
});
return { port, rows };
}
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;
}
async function readMindmapMetrics(page) {
return page.evaluate(() => {
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
const scene = root instanceof HTMLElement ? root.querySelector('[data-testid="leptos-mindmap-island"]') : null;
const overlayLayer = root instanceof HTMLElement ? root.querySelector('[data-testid="mindmap-overlay-layer"]') : null;
const canvasLayer = root instanceof HTMLElement ? root.querySelector('[data-testid="mindmap-canvas-layer"]') : null;
const schemaToolbar = document.querySelector('[data-testid="mindmap-schema-toolbar"]');
const schemaSidebar = document.querySelector('[data-testid="mindmap-schema-sidebar"]');
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"]');
2026-05-13 22:43:16 +08:00
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"]');
const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null;
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = mindmapId ? registry[mindmapId] : null;
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
const canonicalRoot =
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
? snapshot.root
: snapshot;
const snapshotStats = { nodeCount: 0, edgeCount: 0, nodeTexts: [] };
const visit = (node) => {
if (!node || typeof node !== "object" || Array.isArray(node)) return;
snapshotStats.nodeCount += 1;
const text = node.data && typeof node.data.text === "string" ? node.data.text.trim() : "";
if (text) snapshotStats.nodeTexts.push(text);
const children = Array.isArray(node.children) ? node.children : [];
snapshotStats.edgeCount += children.length;
children.forEach(visit);
};
visit(canonicalRoot);
const runtimeNodes = runtime
? Array.from(runtime.querySelectorAll(".smm-node")).filter((node) => {
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}).length
: 0;
const svgPaths = runtime
? Array.from(runtime.querySelectorAll("svg path, svg line, svg polyline")).filter((edge) => {
const rect = edge.getBoundingClientRect();
return rect.width > 0 || rect.height > 0 || edge.getAttribute("d") || edge.getAttribute("points");
}).length
: 0;
const runtimeRect = runtime?.getBoundingClientRect();
const rootRect = root?.getBoundingClientRect();
const overlayRect = overlayLayer?.getBoundingClientRect();
const canvasRect = canvasLayer?.getBoundingClientRect();
return {
errorStage: error instanceof HTMLElement ? error.dataset.stage || "unknown_error" : null,
rootExists: root instanceof HTMLElement,
mindmapId,
runtimeExists: runtime instanceof HTMLElement,
runtimeEngine: runtime instanceof HTMLElement ? runtime.dataset.runtimeEngine || null : null,
runtimeReady: runtime instanceof HTMLElement ? runtime.dataset.runtimeReady === "true" : false,
lastViewEvent:
root instanceof HTMLElement
? root.querySelector('[data-testid="leptos-mindmap-island"]')?.dataset.lastViewEvent || null
: null,
debugChrome:
scene instanceof HTMLElement ? scene.dataset.debugChrome || null : null,
uiShell:
scene instanceof HTMLElement ? scene.dataset.uiShell || null : null,
rustShell: {
mount: rustShellMount instanceof HTMLElement,
mountSource: rustShellMount instanceof HTMLElement ? rustShellMount.dataset.uiShellSource || null : null,
shell: rustShell instanceof HTMLElement,
shellSource: rustShell instanceof HTMLElement ? rustShell.dataset.uiShellSource || null : null,
},
schemaChrome: {
toolbar:
schemaToolbar instanceof HTMLElement &&
schemaToolbar.dataset.schemaSource === "mindmapDefaultUiSchema" &&
schemaToolbar.dataset.uiSource === "leptos-rust-shell",
sidebar:
schemaSidebar instanceof HTMLElement &&
schemaSidebar.dataset.schemaSource === "mindmapDefaultUiSchema" &&
schemaSidebar.dataset.uiSource === "leptos-rust-shell",
navigator:
schemaNavigator instanceof HTMLElement &&
schemaNavigator.dataset.schemaSource === "mindmapDefaultUiSchema" &&
schemaNavigator.dataset.uiSource === "leptos-rust-shell",
count:
schemaCount instanceof HTMLElement &&
schemaCount.dataset.schemaSource === "mindmapDefaultUiSchema" &&
schemaCount.dataset.uiSource === "leptos-rust-shell",
toolbarActions: Array.from(document.querySelectorAll("[data-mindmap-action-id]"))
.map((item) => item instanceof HTMLElement ? item.dataset.mindmapActionId : null)
.filter(Boolean),
disabledActions: Array.from(document.querySelectorAll("[data-mindmap-action-id]"))
.filter((item) => item instanceof HTMLButtonElement && item.disabled)
.map((item) => item.dataset.mindmapActionId)
.filter(Boolean),
sidebarPanels: Array.from(document.querySelectorAll("[data-mindmap-sidebar-panel-id]"))
.map((item) => item instanceof HTMLElement ? item.dataset.mindmapSidebarPanelId : null)
.filter(Boolean),
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,
2026-05-13 22:43:16 +08:00
zoomValue: schemaZoomInput instanceof HTMLInputElement ? schemaZoomInput.value : null,
minimap: Boolean(document.querySelector('[data-testid="mindmap-schema-minimap"]')),
contextMenu:
schemaContextMenu instanceof HTMLElement
? {
kind: schemaContextMenu.dataset.contextMenuKind || null,
actions: Array.from(schemaContextMenu.querySelectorAll("[data-mindmap-context-action-id]"))
.map((item) => item instanceof HTMLElement ? item.dataset.mindmapContextActionId : null)
.filter(Boolean),
}
: null,
},
overlayShell: {
overlay: overlayLayer instanceof HTMLElement,
canvas: canvasLayer instanceof HTMLElement,
rootBox: rootRect
? { width: rootRect.width, height: rootRect.height }
: { width: 0, height: 0 },
overlayBox: overlayRect
? { width: overlayRect.width, height: overlayRect.height }
: { width: 0, height: 0 },
canvasBox: canvasRect
? { width: canvasRect.width, height: canvasRect.height }
: { width: 0, height: 0 },
},
legacyFallback: {
toolbar: Boolean(document.querySelector('[data-testid="mindmap-command-toolbar"]')),
sidebar: Boolean(document.querySelector('[data-testid="mindmap-sidebar"]')),
bottomBar: Boolean(document.querySelector('[data-testid="mindmap-bottom-bar"]')),
},
viewCommandStatus:
scene instanceof HTMLElement ? scene.dataset.viewCommandStatus || null : null,
2026-05-13 22:43:16 +08:00
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 }
: { width: 0, height: 0 },
nodeCount: snapshotStats.nodeCount || runtimeNodes,
edgeCount: snapshotStats.edgeCount || svgPaths,
runtimeNodeCount: runtimeNodes,
svgPathCount: svgPaths,
nodeTexts: snapshotStats.nodeTexts,
};
});
}
async function assertMindmapReady(page, stage) {
await page.locator('[data-testid="mnote-mindmap-editor-root"]').waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
await page.locator('[data-testid="simple-mind-map-runtime"]').waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
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__ || {};
const bridge = mindmapId ? registry[mindmapId] : null;
return (
runtime instanceof HTMLElement &&
runtime.dataset.runtimeEngine === "simple-mind-map" &&
runtime.dataset.runtimeReady === "true" &&
Boolean(bridge)
);
},
null,
{ timeout: UI_TIMEOUT_MS },
);
const metrics = await readMindmapMetrics(page);
if (metrics.errorStage) {
throw new Error(`${metrics.errorStage}: ${stage}`);
}
assert(metrics.rootExists, `${stage}: mnote-mindmap-editor-root 不存在`);
assert(metrics.runtimeExists, `${stage}: runtime_render_failed`);
assert(metrics.runtimeEngine === "simple-mind-map", `${stage}: runtime_engine_failed`);
assert(metrics.runtimeReady, `${stage}: runtime_not_ready`);
assert(metrics.bridgeExists, `${stage}: bridge_not_registered`);
assert(metrics.runtimeBox.width > 0 && metrics.runtimeBox.height > 0, `${stage}: runtime_render_failed`);
assert(metrics.debugChrome === "false", `${stage}: debug_chrome_unexpected`);
assert(metrics.uiShell === "leptos-rust-shell", `${stage}: ui_shell_source_unexpected`);
assert(metrics.rustShell.mount, `${stage}: rust_shell_mount_missing`);
assert(metrics.rustShell.mountSource === "leptos-rust-shell", `${stage}: rust_shell_mount_source_unexpected`);
assert(metrics.rustShell.shell, `${stage}: rust_shell_missing`);
assert(metrics.rustShell.shellSource === "leptos-rust-shell", `${stage}: rust_shell_source_unexpected`);
assert(metrics.schemaChrome.toolbar, `${stage}: ui_shell_missing toolbar`);
assert(metrics.schemaChrome.sidebar, `${stage}: ui_shell_missing sidebar`);
assert(metrics.schemaChrome.navigator, `${stage}: ui_shell_missing navigator`);
assert(metrics.schemaChrome.count, `${stage}: ui_shell_missing count`);
assert(metrics.overlayShell.overlay, `${stage}: floating_overlay_missing overlay`);
assert(metrics.overlayShell.canvas, `${stage}: floating_overlay_missing canvas`);
assert(metrics.runtimeBox.width / Math.max(metrics.overlayShell.rootBox.width, 1) >= 0.9, `${stage}: runtime_width_squeezed`);
assert(metrics.runtimeBox.height / Math.max(metrics.overlayShell.rootBox.height, 1) >= 0.85, `${stage}: runtime_height_squeezed`);
assert(!metrics.legacyFallback.toolbar, `${stage}: legacy_toolbar_unexpected`);
assert(!metrics.legacyFallback.sidebar, `${stage}: legacy_sidebar_unexpected`);
assert(!metrics.legacyFallback.bottomBar, `${stage}: legacy_bottom_bar_unexpected`);
const expectedActions = [
"undo",
"redo",
"insertSiblingAfter",
"insertChild",
"deleteNode",
"tag",
"hyperlink",
"note",
"image",
"icon",
"summary",
"associativeLine",
"formula",
"painter",
"import",
"export",
];
for (const actionId of expectedActions) {
assert(metrics.schemaChrome.toolbarActions.includes(actionId), `${stage}: ui_shell_missing action=${actionId}`);
}
for (const panelId of ["nodeStyle", "baseStyle", "theme", "structure", "outline"]) {
assert(metrics.schemaChrome.sidebarPanels.includes(panelId), `${stage}: ui_shell_missing panel=${panelId}`);
}
assert(/节点/.test(metrics.schemaChrome.countText || ""), `${stage}: ui_shell_missing count_stats`);
2026-05-13 22:43:16 +08:00
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;
}
async function readMindmapSnapshotStats(page, mindmapId) {
return page.evaluate((mindmapId) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
const root =
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
? snapshot.root
: snapshot;
const stats = { nodeCount: 0, edgeCount: 0, nodeTexts: [] };
const visit = (node) => {
if (!node || typeof node !== "object" || Array.isArray(node)) return;
stats.nodeCount += 1;
const text = node.data && typeof node.data.text === "string" ? node.data.text.trim() : "";
if (text) stats.nodeTexts.push(text);
const children = Array.isArray(node.children) ? node.children : [];
stats.edgeCount += children.length;
children.forEach(visit);
};
visit(root);
return stats;
}, mindmapId);
}
2026-05-13 22:43:16 +08:00
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) {
scene.dataset.commandStatus = "smoke-waiting";
delete scene.dataset.lastSchemaAction;
}
});
await page.getByTestId(`mindmap-schema-toolbar-action-${actionId}`).click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
(actionId) => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return (
scene instanceof HTMLElement &&
scene.dataset.lastSchemaAction === actionId &&
scene.dataset.commandStatus === "success"
);
},
actionId,
{ timeout: UI_TIMEOUT_MS },
).catch(async (error) => {
const diagnostics = await page.evaluate((actionId) => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
const button = document.querySelector(`[data-testid="mindmap-schema-toolbar-action-${actionId}"]`);
const errorLayer = document.querySelector('[data-testid="leptos-mindmap-error"]');
return {
sceneDataset: scene instanceof HTMLElement ? { ...scene.dataset } : null,
buttonDisabled: button instanceof HTMLButtonElement ? button.disabled : null,
buttonExists: button instanceof HTMLElement,
errorStage: errorLayer instanceof HTMLElement ? errorLayer.getAttribute("data-stage") : null,
errorText: errorLayer instanceof HTMLElement ? errorLayer.textContent : null,
};
}, actionId);
throw new Error(`command_failed:${actionId}:command_status_timeout:${JSON.stringify(diagnostics)}:${error.message}`);
});
2026-05-13 22:43:16 +08:00
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) {
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.lastSchemaCommand;
}
});
await page.getByTestId(`mindmap-schema-sidebar-option-${optionId}`).click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
(actionId) => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return (
scene instanceof HTMLElement &&
scene.dataset.lastSchemaAction === actionId &&
scene.dataset.commandStatus === "success"
);
},
actionId,
{ timeout: UI_TIMEOUT_MS },
).catch(async (error) => {
const diagnostics = await page.evaluate((optionId) => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
const option = document.querySelector(`[data-testid="mindmap-schema-sidebar-option-${optionId}"]`);
const errorLayer = document.querySelector('[data-testid="leptos-mindmap-error"]');
return {
sceneDataset: scene instanceof HTMLElement ? { ...scene.dataset } : null,
optionExists: option instanceof HTMLElement,
errorStage: errorLayer instanceof HTMLElement ? errorLayer.getAttribute("data-stage") : null,
errorText: errorLayer instanceof HTMLElement ? errorLayer.textContent : null,
};
}, optionId);
throw new Error(`command_failed:${actionId}:${optionId}:command_status_timeout:${JSON.stringify(diagnostics)}:${error.message}`);
});
}
async function verifyReadonlyToolbarState(page, stage) {
await page.getByTestId("mindmap-schema-navigator-action-readonly").click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const insertChild = document.querySelector('[data-testid="mindmap-schema-toolbar-action-insertChild"]');
const deleteNode = document.querySelector('[data-testid="mindmap-schema-toolbar-action-deleteNode"]');
const zoomIn = document.querySelector('[data-testid="mindmap-schema-navigator-action-zoomIn"]');
const centerRoot = document.querySelector('[data-testid="mindmap-schema-navigator-action-centerRoot"]');
return (
insertChild instanceof HTMLButtonElement &&
deleteNode instanceof HTMLButtonElement &&
zoomIn instanceof HTMLButtonElement &&
centerRoot instanceof HTMLButtonElement &&
insertChild.disabled &&
deleteNode.disabled &&
!zoomIn.disabled &&
!centerRoot.disabled
);
},
null,
{ timeout: UI_TIMEOUT_MS },
).catch((error) => {
throw new Error(`ui_shell_missing:${stage}:readonly_disabled_state:${error.message}`);
});
await page.getByTestId("mindmap-schema-navigator-action-readonly").click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const insertChild = document.querySelector('[data-testid="mindmap-schema-toolbar-action-insertChild"]');
return insertChild instanceof HTMLButtonElement && !insertChild.disabled;
},
null,
{ timeout: UI_TIMEOUT_MS },
);
return true;
}
async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
const before = await readMindmapSnapshotStats(page, mindmapId);
2026-05-13 22:43:16 +08:00
const childResponse = await clickToolbarActionAndWaitForCommand(page, mindmapId, "insertChild");
await page.waitForFunction(
({ mindmapId, expected }) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
const root =
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
? snapshot.root
: snapshot;
let count = 0;
const visit = (node) => {
if (!node || typeof node !== "object" || Array.isArray(node)) return;
count += 1;
if (Array.isArray(node.children)) node.children.forEach(visit);
};
visit(root);
return count >= expected;
},
{ mindmapId, expected: before.nodeCount + 1 },
{ timeout: UI_TIMEOUT_MS },
);
const afterChild = await readMindmapSnapshotStats(page, mindmapId);
2026-05-13 22:43:16 +08:00
const siblingResponse = await clickToolbarActionAndWaitForCommand(page, mindmapId, "insertSiblingAfter");
await page.waitForFunction(
({ mindmapId, expected }) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
const root =
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
? snapshot.root
: snapshot;
let count = 0;
const visit = (node) => {
if (!node || typeof node !== "object" || Array.isArray(node)) return;
count += 1;
if (Array.isArray(node.children)) node.children.forEach(visit);
};
visit(root);
return count >= expected;
},
{ mindmapId, expected: afterChild.nodeCount + 1 },
{ timeout: UI_TIMEOUT_MS },
);
const afterSibling = await readMindmapSnapshotStats(page, mindmapId);
2026-05-13 22:43:16 +08:00
const deleteResponse = await clickToolbarActionAndWaitForCommand(page, mindmapId, "deleteNode");
await page.waitForFunction(
({ mindmapId, expectedMax }) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
const root =
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
? snapshot.root
: snapshot;
let count = 0;
const visit = (node) => {
if (!node || typeof node !== "object" || Array.isArray(node)) return;
count += 1;
if (Array.isArray(node.children)) node.children.forEach(visit);
};
visit(root);
return count <= expectedMax;
},
{ mindmapId, expectedMax: afterSibling.nodeCount - 1 },
{ timeout: UI_TIMEOUT_MS },
);
const afterDelete = await readMindmapSnapshotStats(page, mindmapId);
return {
insertChildOk: childResponse.ok() && afterChild.nodeCount >= before.nodeCount + 1,
insertSiblingOk: siblingResponse.ok() && afterSibling.nodeCount >= afterChild.nodeCount + 1,
deleteOk: deleteResponse.ok() && afterDelete.nodeCount <= afterSibling.nodeCount - 1,
2026-05-13 22:43:16 +08:00
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,
afterDeleteNodeCount: afterDelete.nodeCount,
};
}
2026-05-13 22:43:16 +08:00
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");
const themedProjection = await fetchAdapterProjection(page, documentId, mindmapId);
if (themedProjection?.theme !== "classic4") {
throw new Error(`command_failed:setTheme theme=${JSON.stringify(themedProjection?.theme)}`);
}
await page.getByTestId("mindmap-schema-sidebar-tab-structure").click({ timeout: UI_TIMEOUT_MS });
await clickSidebarOptionAndWaitForCommand(page, "layout-mind-map", "setLayout");
const layoutProjection = await fetchAdapterProjection(page, documentId, mindmapId);
if (layoutProjection?.layout !== "mindMap") {
throw new Error(`command_failed:setLayout layout=${JSON.stringify(layoutProjection?.layout)}`);
}
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);
2026-05-13 22:43:16 +08:00
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 });
await clickSidebarOptionAndWaitForCommand(page, "base-curve-line", "painter");
const baseStyleProjection = await fetchAdapterProjection(page, documentId, mindmapId);
const lineStyle = baseStyleProjection?.compatPayload?.style?.map?.lineStyle;
if (lineStyle !== "curve") {
throw new Error(`command_failed:baseStyle compat lineStyle=${JSON.stringify(lineStyle)}`);
}
await page.getByTestId("mindmap-schema-sidebar-tab-outline").click({ timeout: UI_TIMEOUT_MS });
const outlineItems = await page
.locator('[data-testid="mindmap-schema-sidebar-outline-outline"] li')
.evaluateAll((items) => items.map((item) => item.textContent?.trim()).filter(Boolean));
if (outlineItems.length === 0) {
throw new Error("ui_shell_missing:outline_items");
}
return {
themeOk: themedProjection.theme === "classic4",
layoutOk: layoutProjection.layout === "mindMap",
2026-05-13 22:43:16 +08:00
nodeStyleCompatOk: activeNodeFill === "#dbeafe",
nodeStyleTargetNodeId: activeNodeId,
baseStyleCompatOk: lineStyle === "curve",
outlineDerivedOk: outlineItems.length > 0,
outlineItems,
};
}
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 fetchAdapterProjection(page, documentId, mindmapId) {
return page.evaluate(
async ({ documentId, mindmapId }) => {
const response = await fetch(
`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`,
{ headers: { Accept: "application/json" } },
);
const payload = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(`projection_load_failed:${response.status}`);
}
if (payload && typeof payload === "object" && !Array.isArray(payload) && payload.result) {
return payload.result;
}
return payload;
},
{ documentId, mindmapId },
);
}
async function editRootNode(page, documentId, mindmapId, text) {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`) &&
response.request().method() === "POST",
{ timeout: UI_TIMEOUT_MS },
);
await page.evaluate(
async ({ documentId, mindmapId, text }) => {
const response = await fetch(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
commandName: "mindmap.command.apply",
documentId,
mindmapId,
commands: [{ type: "updateText", mindmapId, nodeId: "root", text }],
}),
});
if (!response.ok) {
throw new Error(`command_failed:${response.status}`);
}
},
{ documentId, mindmapId, text },
);
const response = await responsePromise;
if (!response.ok()) {
throw new Error(`command_failed:${response.status()}`);
}
const projection = await fetchAdapterProjection(page, documentId, mindmapId);
const root = projection && typeof projection === "object" && projection.root ? projection.root : projection;
const rootText = root?.data?.text;
if (rootText !== text) {
throw new Error(`command_failed:update_text_projection_mismatch text=${JSON.stringify(rootText)}`);
}
}
async function zoomMindmapAndVerifyViewPatch(page, documentId, mindmapId) {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`) &&
response.request().method() === "POST" &&
response.request().postData()?.includes('"type":"patchView"'),
{ timeout: UI_TIMEOUT_MS },
);
await page.getByTestId("mindmap-schema-navigator-action-zoomIn").click({ timeout: UI_TIMEOUT_MS });
const response = await responsePromise;
if (!response.ok()) {
throw new Error(`view_command_failed:${response.status()}`);
}
await page.waitForFunction(
() => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return scene instanceof HTMLElement && scene.dataset.viewCommandStatus === "success";
},
null,
{ timeout: UI_TIMEOUT_MS },
);
const projection = await fetchAdapterProjection(page, documentId, mindmapId);
const scale = projection?.view?.state?.scale;
if (!(typeof scale === "number" && scale > 1)) {
const requestPayload = JSON.parse(response.request().postData() || "null");
throw new Error(
`view_patch_failed scale=${scale} view=${JSON.stringify(projection?.view ?? null)} patch=${JSON.stringify(
requestPayload?.commands?.[0]?.patch ?? null,
)}`,
);
}
return scale;
}
async function centerRootAndVerifyViewPatch(page, documentId, mindmapId) {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`) &&
response.request().method() === "POST" &&
response.request().postData()?.includes('"type":"patchView"'),
{ timeout: UI_TIMEOUT_MS },
);
await page.getByTestId("mindmap-schema-navigator-action-centerRoot").click({ timeout: UI_TIMEOUT_MS });
const response = await responsePromise;
if (!response.ok()) {
throw new Error(`view_command_failed:centerRoot:${response.status()}`);
}
await page.waitForFunction(
() => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return scene instanceof HTMLElement && scene.dataset.viewCommandStatus === "success";
},
null,
{ timeout: UI_TIMEOUT_MS },
);
return true;
}
async function exerciseNavigatorActions(page, documentId, mindmapId) {
const centerRootOk = await centerRootAndVerifyViewPatch(page, documentId, mindmapId);
const zoomScale = await zoomMindmapAndVerifyViewPatch(page, documentId, mindmapId);
2026-05-13 22:43:16 +08:00
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(
() => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return scene instanceof HTMLElement && scene.dataset.searchStatus === "ready" && scene.dataset.lastSearchQuery === "KMIND";
},
null,
{ timeout: UI_TIMEOUT_MS },
);
await page.getByTestId("mindmap-schema-navigator-action-minimap").click({ timeout: UI_TIMEOUT_MS });
await page.getByTestId("mindmap-schema-minimap").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return {
centerRootOk,
zoomOk: typeof zoomScale === "number" && zoomScale > 1,
searchOk: true,
minimapOk: true,
zoomScale,
};
}
async function openContextMenu(page, targetKind) {
if (targetKind === "node") {
const node = page.locator('[data-testid="simple-mind-map-runtime"] .smm-node').first();
await node.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await node.click({ timeout: UI_TIMEOUT_MS });
await node.click({ button: "right", timeout: UI_TIMEOUT_MS });
} else {
await page.getByTestId("simple-mind-map-runtime").click({ button: "right", position: { x: 36, y: 36 }, timeout: UI_TIMEOUT_MS });
}
await page.getByTestId("mindmap-schema-context-menu").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const metrics = await readMindmapMetrics(page);
assert(metrics.schemaChrome.contextMenu?.kind === targetKind, `ui_shell_missing:context_menu_kind expected=${targetKind} actual=${metrics.schemaChrome.contextMenu?.kind}`);
return metrics.schemaChrome.contextMenu.actions;
}
async function exerciseContextMenuActions(page, mindmapId) {
const nodeActions = await openContextMenu(page, "node");
for (const actionId of ["insertChild", "insertSiblingAfter", "deleteNode", "summary", "associativeLine", "expandCollapse", "copyNodeText"]) {
assert(nodeActions.includes(actionId), `ui_shell_missing:node_context_action=${actionId}`);
}
await page.getByTestId("mindmap-schema-context-menu-action-copyNodeText").click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return scene instanceof HTMLElement && scene.dataset.lastContextMenuAction === "copyNodeText" && (scene.dataset.copiedNodeText || "").length > 0;
},
null,
{ timeout: UI_TIMEOUT_MS },
);
const before = await readMindmapSnapshotStats(page, mindmapId);
await openContextMenu(page, "node");
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 page.getByTestId("mindmap-schema-context-menu-action-insertChild").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 },
);
const afterInsert = await readMindmapSnapshotStats(page, mindmapId);
const canvasActions = await openContextMenu(page, "canvas");
for (const actionId of ["centerRoot", "fitView", "search", "readonly"]) {
assert(canvasActions.includes(actionId), `ui_shell_missing:canvas_context_action=${actionId}`);
}
assert(!canvasActions.includes("insertChild"), "ui_shell_missing:canvas_context_hides_node_action");
return {
nodeMenuOk: nodeActions.includes("insertChild") && nodeActions.includes("copyNodeText"),
canvasMenuOk: canvasActions.includes("centerRoot") && canvasActions.includes("readonly") && !canvasActions.includes("insertChild"),
copyTextOk: true,
insertChildOk: afterInsert.nodeCount >= before.nodeCount + 1,
beforeNodeCount: before.nodeCount,
afterInsertNodeCount: afterInsert.nodeCount,
};
}
async function waitForMindmapText(page, mindmapId, text, stage) {
await page.waitForFunction(
({ mindmapId, text }) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
const root =
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
? snapshot.root
: snapshot;
const stack = root ? [root] : [];
while (stack.length > 0) {
const node = stack.pop();
if (!node || typeof node !== "object" || Array.isArray(node)) continue;
if (node.data && node.data.text === text) return true;
if (Array.isArray(node.children)) stack.push(...node.children);
}
return false;
},
{ mindmapId, text },
{ timeout: UI_TIMEOUT_MS },
).catch((error) => {
throw new Error(`reload_mismatch:${stage}:${error.message}`);
});
}
async function main() {
await fs.rm(OUTPUT_DIR, { recursive: true, force: true });
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const portReport = [checkPort(3000), checkPort(8000)];
const screenshots = [];
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const page = await context.newPage();
const createdIds = [];
let result = {
ok: false,
task: TASK,
baseUrl: BASE_URL,
documentId: null,
mindmapId: null,
nodeCount: 0,
edgeCount: 0,
toolbarActions: null,
sidebarActions: null,
kernelRevision: null,
adapterSource: null,
viewScale: null,
navigatorActions: null,
contextMenuActions: null,
2026-05-13 22:43:16 +08:00
keyboardActions: null,
screenshots,
ports: portReport,
};
try {
await ensureAuthenticated(page, context.request);
const doc = await createTempDocument(context.request, null);
createdIds.push(doc.documentId);
const title = `task166-mindmap-${Date.now().toString().slice(-6)}`;
await renameDocument(context.request, doc.workspaceId, doc.documentId, title);
result.documentId = doc.documentId;
await openDocument(page, doc.workspaceId, doc.documentId);
await insertMindmapThroughSlash(page);
const inserted = await assertMindmapReady(page, "insert");
screenshots.push(await screenshot(page, "01-after-insert-floating-overlay"));
screenshots.push(await screenshot(page, "toolbar-actions"));
const mindmapId = inserted.mindmapId;
assert(mindmapId, "insert: mindmapId 缺失");
result.mindmapId = mindmapId;
const insertedProjection = await fetchAdapterProjection(page, doc.documentId, mindmapId);
result.kernelRevision =
typeof insertedProjection?.kernelRevision === "number" ? insertedProjection.kernelRevision : null;
result.adapterSource =
typeof insertedProjection?.source === "string"
? insertedProjection.source
: typeof insertedProjection?.compatPayload?.source === "string"
? insertedProjection.compatPayload.source
: null;
const editedText = `KMIND-${Date.now().toString().slice(-5)}`;
await editRootNode(page, doc.documentId, mindmapId, editedText);
const edited = await assertMindmapReady(page, "edit");
screenshots.push(await screenshot(page, "02-after-edit"));
await verifyReadonlyToolbarState(page, "toolbar");
const toolbarActions = await exerciseToolbarNodeActions(page, doc.documentId, mindmapId);
assert(toolbarActions.insertChildOk, "command_failed:insertChild");
assert(toolbarActions.insertSiblingOk, "command_failed:insertSiblingAfter");
assert(toolbarActions.deleteOk, "command_failed:deleteNode");
2026-05-13 22:43:16 +08:00
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"));
2026-05-13 22:43:16 +08:00
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"],
["theme", "sidebar-theme"],
["structure", "sidebar-structure"],
["outline", "sidebar-outline"],
]) {
await page.getByTestId(`mindmap-schema-sidebar-tab-${panelId}`).click({ timeout: UI_TIMEOUT_MS });
screenshots.push(await screenshot(page, name));
}
const sidebarActions = await exerciseSidebarActions(page, doc.documentId, mindmapId);
assert(sidebarActions.themeOk, "command_failed:setTheme");
assert(sidebarActions.layoutOk, "command_failed:setLayout");
assert(sidebarActions.nodeStyleCompatOk, "command_failed:nodeStyleCompat");
assert(sidebarActions.baseStyleCompatOk, "command_failed:baseStyleCompat");
assert(sidebarActions.outlineDerivedOk, "ui_shell_missing:outline");
screenshots.push(await screenshot(page, "sidebar-actions"));
screenshots.push(await screenshot(page, "navigator-default"));
const navigatorActions = await exerciseNavigatorActions(page, doc.documentId, mindmapId);
assert(navigatorActions.centerRootOk, "view_command_failed:centerRoot");
assert(navigatorActions.zoomOk, "view_command_failed:zoomIn");
assert(navigatorActions.searchOk, "ui_shell_missing:search");
assert(navigatorActions.minimapOk, "ui_shell_missing:minimap");
screenshots.push(await screenshot(page, "navigator-zoomed"));
screenshots.push(await screenshot(page, "navigator-minimap"));
const contextMenuActions = await exerciseContextMenuActions(page, mindmapId);
assert(contextMenuActions.nodeMenuOk, "ui_shell_missing:node_context_menu");
assert(contextMenuActions.canvasMenuOk, "ui_shell_missing:canvas_context_menu");
assert(contextMenuActions.copyTextOk, "command_failed:copyNodeText");
assert(contextMenuActions.insertChildOk, "command_failed:context_insertChild");
screenshots.push(await screenshot(page, "context-menu-actions"));
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
const reloaded = await assertMindmapReady(page, "reload");
await waitForMindmapText(page, mindmapId, editedText, "reload");
const reloadStats = await readMindmapSnapshotStats(page, mindmapId);
const expectedReloadNodeCount = contextMenuActions?.afterInsertNodeCount ?? toolbarActions.afterDeleteNodeCount;
assert(
reloadStats.nodeCount === expectedReloadNodeCount,
`reload_mismatch:toolbar_node_count expected=${expectedReloadNodeCount} actual=${reloadStats.nodeCount}`,
);
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)}`);
2026-05-13 22:43:16 +08:00
const reloadNodeStyleFill = findNodeFillColorByUid(reloadProjection?.root, sidebarActions.nodeStyleTargetNodeId);
assert(
2026-05-13 22:43:16 +08:00
reloadNodeStyleFill === "#dbeafe",
`reload_mismatch:node_style active=${JSON.stringify(sidebarActions.nodeStyleTargetNodeId)} actual=${JSON.stringify(reloadProjection?.root?.data)}`,
);
assert(
reloadProjection?.compatPayload?.style?.map?.lineStyle === "curve",
`reload_mismatch:base_style actual=${JSON.stringify(reloadProjection?.compatPayload?.style)}`,
);
screenshots.push(await screenshot(page, "04-after-reload"));
result = {
...result,
ok: true,
nodeCount: reloaded.nodeCount,
edgeCount: reloaded.edgeCount,
toolbarActions,
2026-05-13 22:43:16 +08:00
keyboardActions,
sidebarActions,
navigatorActions,
contextMenuActions,
kernelRevision: result.kernelRevision,
adapterSource: result.adapterSource,
viewScale: navigatorActions.zoomScale,
};
await writeResult(result);
process.stdout.write(`${JSON.stringify({ ...result, resultPath: RESULT_PATH }, null, 2)}\n`);
} catch (error) {
const metrics = await readMindmapMetrics(page).catch(() => null);
const failureStage =
metrics?.errorStage ||
(error instanceof Error && /projection_load_failed|adapter_init_failed|runtime_render_failed|ui_shell_missing|command_failed|reload_mismatch/.test(error.message)
? error.message
: "runtime_render_failed");
result = {
...result,
ok: false,
error: error instanceof Error ? error.message : String(error),
failureStage,
nodeCount: metrics?.nodeCount ?? result.nodeCount,
edgeCount: metrics?.edgeCount ?? result.edgeCount,
};
screenshots.push(await screenshot(page, "99-failure").catch(() => null));
await writeResult(result);
throw error;
} finally {
if (createdIds.length > 0) {
await cleanupDocuments(context.request, createdIds).catch(() => undefined);
}
await page.close().catch(() => undefined);
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
if (require.main === module) {
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});
}