20260513 mindmap优化01

This commit is contained in:
lix-2026
2026-05-13 22:43:16 +08:00
parent 17c003976b
commit b4a452a8b7
89 changed files with 11557 additions and 707 deletions
+233 -12
View File
@@ -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,