#!/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"]'); 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, 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, 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`); 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); } 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}`); }); 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); 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); 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); 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, 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, }; } 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); 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", 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); 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, 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"); 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"], ["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)}`); const reloadNodeStyleFill = findNodeFillColorByUid(reloadProjection?.root, sidebarActions.nodeStyleTargetNodeId); assert( 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, 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); }); }