#!/usr/bin/env node "use strict"; const fs = require("node:fs/promises"); const path = require("node:path"); const { chromium } = require("playwright"); const { BASE_URL, UI_TIMEOUT_MS, assert, cleanupDocuments, createTempDocument, ensureAuthenticated, openDocument, renameDocument, } = require("./tree-shell-smoke-helpers"); const TASK = "task167-mindmap-kmind-parity-smoke"; const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); const STAGE = process.argv.includes("--stage") ? process.argv[process.argv.indexOf("--stage") + 1] : "all"; 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 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 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 shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); 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" && shell instanceof HTMLElement && shell.dataset.uiShellSource === "leptos-rust-shell" && Boolean(bridge) ); }, null, { timeout: UI_TIMEOUT_MS }, ).catch((error) => { throw new Error(`mindmap_not_ready:${stage}:${error.message}`); }); } async function readMindmapIdentity(page) { return page.evaluate(() => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); return { mindmapId: root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null, }; }); } async function readToolbarMetrics(page) { return page.evaluate(() => { const toolbar = document.querySelector('[data-testid="mindmap-schema-toolbar"]'); const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const mainCluster = document.querySelector('[data-testid="mindmap-schema-toolbar-cluster-main"]'); const fileCluster = document.querySelector('[data-testid="mindmap-schema-toolbar-cluster-file"]'); const moreButton = document.querySelector('[data-testid="mindmap-schema-toolbar-action-more"]'); const moreMenu = document.querySelector('[data-testid="mindmap-schema-toolbar-more-menu"]'); const buttons = Array.from(document.querySelectorAll('[data-testid^="mindmap-schema-toolbar-action-"]')).filter( (node) => node instanceof HTMLElement && node.getBoundingClientRect().width > 0, ); const rect = toolbar instanceof HTMLElement ? toolbar.getBoundingClientRect() : null; const topValues = buttons.map((node) => Math.round(node.getBoundingClientRect().top)); return { toolbarExists: toolbar instanceof HTMLElement, shellExists: shell instanceof HTMLElement, height: rect ? rect.height : 0, width: rect ? rect.width : 0, toolbarRows: new Set(topValues).size, visibleActions: shell instanceof HTMLElement && shell.dataset.toolbarVisibleActions ? shell.dataset.toolbarVisibleActions.split(",").filter(Boolean) : [], overflowActions: shell instanceof HTMLElement && shell.dataset.toolbarOverflowActions ? shell.dataset.toolbarOverflowActions.split(",").filter(Boolean) : [], moreOpen: shell instanceof HTMLElement ? shell.dataset.toolbarMoreOpen === "true" : false, mainCluster: mainCluster instanceof HTMLElement, fileCluster: fileCluster instanceof HTMLElement, moreButton: moreButton instanceof HTMLButtonElement, moreMenu: moreMenu instanceof HTMLElement, labels: buttons.map((node) => (node.textContent || "").trim()), iconKeys: buttons.map((node) => (node instanceof HTMLElement ? node.dataset.iconKey || null : null)), }; }); } async function verifyToolbarStage(page) { await page.setViewportSize({ width: 1440, height: 960 }); const desktopMetrics = await readToolbarMetrics(page); assert(desktopMetrics.toolbarExists, "toolbar_missing"); assert(desktopMetrics.shellExists, "rust_shell_missing"); assert(desktopMetrics.mainCluster, "toolbar_main_cluster_missing"); assert(desktopMetrics.fileCluster, "toolbar_file_cluster_missing"); assert(desktopMetrics.height <= 72, `toolbar_height_too_large:${desktopMetrics.height}`); assert(desktopMetrics.toolbarRows <= 1, `toolbar_not_single_row:${desktopMetrics.toolbarRows}`); assert( desktopMetrics.visibleActions.join(",").startsWith("undo,redo,editNode,insertSiblingAfter,deleteNode,insertChild"), "toolbar_order_unexpected", ); assert(desktopMetrics.labels.every((label) => !/(palette|sliders|layout)/i.test(label)), "toolbar_internal_icon_key_visible"); await page.setViewportSize({ width: 900, height: 760 }); await page.waitForTimeout(500); const narrowMetrics = await readToolbarMetrics(page); assert(narrowMetrics.height <= 72, `narrow_toolbar_height_too_large:${narrowMetrics.height}`); assert(narrowMetrics.toolbarRows <= 1, `narrow_toolbar_not_single_row:${narrowMetrics.toolbarRows}`); assert(narrowMetrics.overflowActions.length > 0, "narrow_toolbar_overflow_missing"); assert(narrowMetrics.moreButton, "toolbar_more_button_missing"); await page.getByTestId("mindmap-schema-toolbar-action-more").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const menu = document.querySelector('[data-testid="mindmap-schema-toolbar-more-menu"]'); return shell instanceof HTMLElement && shell.dataset.toolbarMoreOpen === "true" && menu instanceof HTMLElement; }, null, { timeout: UI_TIMEOUT_MS }, ); const menuMetrics = await readToolbarMetrics(page); assert(menuMetrics.moreOpen, "toolbar_more_not_open"); assert(menuMetrics.moreMenu, "toolbar_more_menu_missing"); await page.keyboard.press("Escape"); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const menu = document.querySelector('[data-testid="mindmap-schema-toolbar-more-menu"]'); return shell instanceof HTMLElement && shell.dataset.toolbarMoreOpen === "false" && !(menu instanceof HTMLElement); }, null, { timeout: UI_TIMEOUT_MS }, ); const closedMetrics = await readToolbarMetrics(page); assert(!closedMetrics.moreOpen, "toolbar_more_escape_not_closed"); return { desktop: desktopMetrics, narrow: narrowMetrics, menu: menuMetrics, closed: closedMetrics, }; } async function verifyFullscreenStage(page) { await page.setViewportSize({ width: 1440, height: 960 }); await page.waitForTimeout(200); const before = await readToolbarMetrics(page); assert(before.toolbarExists, "toolbar_missing_before_fullscreen"); const root = page.getByTestId("mnote-mindmap-editor-root"); const rootBox = await root.boundingBox(); assert(rootBox && rootBox.width > 0 && rootBox.height > 0, "fullscreen_root_box_missing"); const fullscreenButton = page.getByTestId("mindmap-schema-navigator-action-fullscreenCanvas"); await fullscreenButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await fullscreenButton.click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); return ( root instanceof HTMLElement && shell instanceof HTMLElement && scene instanceof HTMLElement && (document.fullscreenElement === root || root.contains(document.fullscreenElement)) && shell.dataset.fullscreenActive === "true" && scene.dataset.fullscreenActive === "true" ); }, null, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction( () => { const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); return scene instanceof HTMLElement && scene.dataset.fullscreenResizeStatus === "success"; }, null, { timeout: UI_TIMEOUT_MS }, ); const active = await page.evaluate(() => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const toolbar = document.querySelector('[data-testid="mindmap-schema-toolbar"]'); const sidebar = document.querySelector('[data-testid="mindmap-schema-sidebar"]'); const navigator = document.querySelector('[data-testid="mindmap-schema-navigator"]'); return { fullscreenElementIsRoot: root instanceof HTMLElement && document.fullscreenElement === root, fullscreenElementInsideRoot: root instanceof HTMLElement && Boolean(document.fullscreenElement) && root.contains(document.fullscreenElement), shellFullscreenActive: shell instanceof HTMLElement ? shell.dataset.fullscreenActive === "true" : false, toolbarVisible: toolbar instanceof HTMLElement && toolbar.getBoundingClientRect().height > 0, sidebarVisible: sidebar instanceof HTMLElement && sidebar.getBoundingClientRect().height > 0, navigatorVisible: navigator instanceof HTMLElement && navigator.getBoundingClientRect().height > 0, }; }); assert(active.fullscreenElementIsRoot || active.fullscreenElementInsideRoot, "fullscreen_element_unexpected"); assert(active.shellFullscreenActive, "fullscreen_shell_state_missing"); assert(active.toolbarVisible && active.sidebarVisible && active.navigatorVisible, "fullscreen_chrome_not_visible"); await page.keyboard.press("Escape"); await page.waitForFunction(() => document.fullscreenElement === null, null, { timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.fullscreenActive === "false"; }, null, { timeout: UI_TIMEOUT_MS }, ); const after = await page.evaluate(() => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return { fullscreenElement: document.fullscreenElement === null, shellFullscreenActive: shell instanceof HTMLElement ? shell.dataset.fullscreenActive === "true" : null, chromeVisibility: shell instanceof HTMLElement ? shell.dataset.chromeVisibility || null : null, }; }); assert(after.fullscreenElement, "fullscreen_escape_exit_failed"); assert(after.shellFullscreenActive === false, "fullscreen_shell_state_not_restored"); assert(after.chromeVisibility === "visible", `fullscreen_visible_state_not_restored:${after.chromeVisibility}`); await page.mouse.move(rootBox.x + rootBox.width / 2, rootBox.y + rootBox.height / 2); await page.mouse.move(Math.max(4, rootBox.x - 24), Math.max(4, rootBox.y - 24)); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.chromeVisibility === "hiddenByPointerLeave"; }, null, { timeout: UI_TIMEOUT_MS }, ); await page.evaluate(async () => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); if (root instanceof HTMLElement && typeof root.requestFullscreen === "function") { await root.requestFullscreen(); } }); await page.waitForFunction( () => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); return root instanceof HTMLElement && document.fullscreenElement === root; }, null, { timeout: UI_TIMEOUT_MS }, ); await page.evaluate(async () => { if (document.fullscreenElement) { await document.exitFullscreen(); } }); await page.waitForFunction(() => document.fullscreenElement === null, null, { timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.fullscreenActive === "false"; }, null, { timeout: UI_TIMEOUT_MS }, ); const hiddenAfterExit = await page.evaluate(() => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return { chromeVisibility: shell instanceof HTMLElement ? shell.dataset.chromeVisibility || null : null, shellFullscreenActive: shell instanceof HTMLElement ? shell.dataset.fullscreenActive === "true" : null, }; }); assert(hiddenAfterExit.shellFullscreenActive === false, "fullscreen_hidden_shell_state_not_restored"); assert( hiddenAfterExit.chromeVisibility === "hiddenByPointerLeave", `fullscreen_hidden_state_unexpected:${hiddenAfterExit.chromeVisibility}`, ); return { before, active, after, hiddenAfterExit }; } async function readChromeVisibilityMetrics(page) { return page.evaluate(() => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const toolbar = document.querySelector('[data-testid="mindmap-schema-toolbar"]'); const sidebar = document.querySelector('[data-testid="mindmap-schema-sidebar"]'); const navigator = document.querySelector('[data-testid="mindmap-schema-navigator"]'); const minimap = document.querySelector('[data-testid="mindmap-schema-minimap"]'); return { rootBox: root instanceof HTMLElement ? root.getBoundingClientRect().toJSON?.() ?? null : null, sceneChromeVisibility: scene instanceof HTMLElement ? scene.dataset.chromeVisibility || null : null, shellChromeVisibility: shell instanceof HTMLElement ? shell.dataset.chromeVisibility || null : null, toolbarVisible: toolbar instanceof HTMLElement && toolbar.getBoundingClientRect().height > 0, sidebarVisible: sidebar instanceof HTMLElement && sidebar.getBoundingClientRect().height > 0, navigatorVisible: navigator instanceof HTMLElement && navigator.getBoundingClientRect().height > 0, minimapVisible: minimap instanceof HTMLElement && minimap.getBoundingClientRect().height > 0, }; }); } async function verifyChromeHideStage(page) { const root = page.getByTestId("mnote-mindmap-editor-root"); await root.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); const box = await root.boundingBox(); assert(box && box.width > 0 && box.height > 0, "mindmap_root_box_missing"); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.chromeVisibility === "visible"; }, null, { timeout: UI_TIMEOUT_MS }, ); const before = await readChromeVisibilityMetrics(page); assert(before.toolbarVisible && before.sidebarVisible && before.navigatorVisible, "chrome_not_visible_before_leave"); await page.mouse.move(Math.max(4, box.x - 24), Math.max(4, box.y - 24)); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.chromeVisibility === "hiddenByPointerLeave"; }, null, { timeout: UI_TIMEOUT_MS }, ); const hidden = await readChromeVisibilityMetrics(page); assert(!hidden.toolbarVisible && !hidden.sidebarVisible && !hidden.navigatorVisible, "chrome_not_hidden_after_leave"); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.waitForTimeout(300); const afterEnter = await readChromeVisibilityMetrics(page); assert(afterEnter.shellChromeVisibility === "hiddenByPointerLeave", "chrome_restored_by_pointer_enter"); await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.chromeVisibility === "visible"; }, null, { timeout: UI_TIMEOUT_MS }, ); const restored = await readChromeVisibilityMetrics(page); assert(restored.toolbarVisible && restored.sidebarVisible && restored.navigatorVisible, "chrome_not_restored_after_click"); return { before, hidden, afterEnter, restored }; } async function readContextMenuMetrics(page) { return page.evaluate(() => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const menu = document.querySelector('[data-testid="mindmap-schema-context-menu"]'); const buttons = Array.from( document.querySelectorAll('[data-testid^="mindmap-schema-context-menu-action-"]'), ).filter((node) => node instanceof HTMLButtonElement); return { chromeVisibility: shell instanceof HTMLElement ? shell.dataset.chromeVisibility || null : null, contextMenuKind: scene instanceof HTMLElement ? scene.dataset.contextMenuKind || null : null, menuVisible: menu instanceof HTMLElement && menu.getBoundingClientRect().width > 0, itemIds: buttons.map((button) => button.dataset.mindmapContextActionId || null), disabledIds: buttons .filter((button) => button.disabled || button.dataset.disabled === "true") .map((button) => button.dataset.mindmapContextActionId || null), }; }); } async function findCanvasEmptyPoint(page) { const point = await page.evaluate(() => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); if (!(root instanceof HTMLElement)) return null; const rootRect = root.getBoundingClientRect(); const blockers = Array.from( document.querySelectorAll( '.smm-node,[data-testid="mindmap-schema-toolbar"],[data-testid="mindmap-schema-sidebar"],[data-testid="mindmap-schema-navigator"],[data-testid="mindmap-schema-context-menu"]', ), ) .filter((node) => node instanceof HTMLElement) .map((node) => node.getBoundingClientRect()) .filter((rect) => rect.width > 0 && rect.height > 0); const padding = 36; for (let y = rootRect.top + padding; y < rootRect.bottom - padding; y += 24) { for (let x = rootRect.left + padding; x < rootRect.right - padding; x += 24) { const blocked = blockers.some((rect) => x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom); if (!blocked) return { x, y }; } } return { x: rootRect.left + rootRect.width * 0.5, y: rootRect.top + rootRect.height * 0.75, }; }); assert(point && typeof point.x === "number" && typeof point.y === "number", "context_menu_canvas_point_missing"); return point; } async function verifyContextMenuStage(page) { const rootNode = page.locator(".smm-node").first(); await rootNode.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await rootNode.click({ button: "right", timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const menu = document.querySelector('[data-testid="mindmap-schema-context-menu"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); return ( menu instanceof HTMLElement && scene instanceof HTMLElement && scene.dataset.contextMenuKind === "node" ); }, null, { timeout: UI_TIMEOUT_MS }, ); const nodeMenu = await readContextMenuMetrics(page); assert(nodeMenu.menuVisible, "node_context_menu_not_visible"); assert(nodeMenu.contextMenuKind === "node", `node_context_menu_kind_unexpected:${nodeMenu.contextMenuKind}`); assert( nodeMenu.itemIds.join(",") === "insertChild,insertSiblingAfter,deleteNode,summary,associativeLine,expandCollapse,copyNodeText", `node_context_menu_items_unexpected:${nodeMenu.itemIds.join(",")}`, ); assert(nodeMenu.disabledIds.includes("deleteNode"), `root_delete_not_disabled:${nodeMenu.disabledIds.join(",")}`); const rootBox = await page.getByTestId("mnote-mindmap-editor-root").boundingBox(); assert(rootBox && rootBox.width > 0 && rootBox.height > 0, "context_menu_root_box_missing"); await page.mouse.move(Math.max(4, rootBox.x - 24), Math.max(4, rootBox.y - 24)); await page.waitForTimeout(250); const afterLeave = await readContextMenuMetrics(page); assert(afterLeave.menuVisible, "context_menu_closed_after_pointerleave"); assert(afterLeave.chromeVisibility === "visible", `chrome_hidden_while_context_menu_open:${afterLeave.chromeVisibility}`); await page.keyboard.press("Escape"); await page.waitForFunction( () => !(document.querySelector('[data-testid="mindmap-schema-context-menu"]') instanceof HTMLElement), null, { timeout: UI_TIMEOUT_MS }, ); const afterEscape = await readContextMenuMetrics(page); assert(!afterEscape.menuVisible, "context_menu_escape_not_closed"); const point = await findCanvasEmptyPoint(page); await page.mouse.click(point.x, point.y, { button: "right" }); await page.waitForFunction( () => { const menu = document.querySelector('[data-testid="mindmap-schema-context-menu"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); return ( menu instanceof HTMLElement && scene instanceof HTMLElement && scene.dataset.contextMenuKind === "canvas" ); }, null, { timeout: UI_TIMEOUT_MS }, ); const canvasMenu = await readContextMenuMetrics(page); assert(canvasMenu.contextMenuKind === "canvas", `canvas_context_menu_kind_unexpected:${canvasMenu.contextMenuKind}`); assert( canvasMenu.itemIds.join(",") === "centerRoot,fitView,search,readonly,showMenu", `canvas_context_menu_items_unexpected:${canvasMenu.itemIds.join(",")}`, ); assert(!canvasMenu.itemIds.includes("insertChild"), "canvas_context_menu_contains_insert_child"); assert(!canvasMenu.itemIds.includes("deleteNode"), "canvas_context_menu_contains_delete_node"); return { nodeMenu, afterLeave, afterEscape, canvasMenu }; } async function readThemeMetrics(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 mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null; const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = mindmapId ? registry[mindmapId] : null; const instance = bridge?.instance || null; const themeConfig = typeof instance?.getThemeConfig === "function" ? instance.getThemeConfig() : null; const nodeRects = Array.from(document.querySelectorAll(".smm-node")) .filter((node) => node instanceof Element) .map((node) => node.getBoundingClientRect()) .filter((rect) => rect.width > 0 && rect.height > 0) .map((rect) => ({ left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height, })); const paths = Array.from(runtime?.querySelectorAll?.("svg path") || []).filter( (node) => node instanceof SVGPathElement && node.getBoundingClientRect().width + node.getBoundingClientRect().height > 0, ); let overlaps = 0; for (let i = 0; i < nodeRects.length; i += 1) { for (let j = i + 1; j < nodeRects.length; j += 1) { const a = nodeRects[i]; const b = nodeRects[j]; const width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left)); const height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top)); if (width * height > 4) overlaps += 1; } } return { layout: typeof instance?.getLayout === "function" ? instance.getLayout() : null, theme: typeof instance?.getTheme === "function" ? instance.getTheme() : null, themeConfig, nodeCount: nodeRects.length, pathCount: paths.length, overlaps, }; }); } async function verifyThemeStage(page) { const initial = await readThemeMetrics(page); assert(initial.layout === "logicalStructure", `theme_layout_unexpected:${initial.layout}`); assert(initial.theme === "default", `theme_template_unexpected:${initial.theme}`); assert(initial.themeConfig?.root?.fillColor === "#e25563", `theme_root_fill_unexpected:${initial.themeConfig?.root?.fillColor}`); assert(initial.themeConfig?.root?.color === "#ffffff", `theme_root_color_unexpected:${initial.themeConfig?.root?.color}`); assert(initial.themeConfig?.second?.fillColor === "#4f7df3", `theme_second_fill_unexpected:${initial.themeConfig?.second?.fillColor}`); assert(initial.themeConfig?.second?.color === "#ffffff", `theme_second_color_unexpected:${initial.themeConfig?.second?.color}`); assert(initial.themeConfig?.node?.color === "#315aa9", `theme_node_color_unexpected:${initial.themeConfig?.node?.color}`); assert( initial.themeConfig?.generalizationLineColor === "#ef6a5b", `theme_generalization_line_unexpected:${initial.themeConfig?.generalizationLineColor}`, ); assert(initial.themeConfig?.lineStyle === "curve", `theme_line_style_unexpected:${initial.themeConfig?.lineStyle}`); assert(initial.nodeCount >= 2, `theme_node_count_too_small:${initial.nodeCount}`); assert(initial.pathCount > 0, `theme_path_count_empty:${initial.pathCount}`); assert(initial.overlaps === 0, `theme_nodes_overlap:${initial.overlaps}`); return { initial }; } async function readSidebarMetrics(page) { return page.evaluate(() => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const rail = document.querySelector('[data-testid="mindmap-schema-sidebar-rail"]'); const drawer = document.querySelector('[data-testid="mindmap-schema-sidebar-drawer"]'); const title = document.querySelector('[data-testid="mindmap-schema-sidebar-title"]'); const closeButton = document.querySelector('[data-testid="mindmap-schema-sidebar-close"]'); const hideHandle = document.querySelector('[data-testid="mindmap-schema-sidebar-hide-handle"]'); const restoreHandle = document.querySelector('[data-testid="mindmap-schema-sidebar-restore-handle"]'); const structureCards = Array.from( document.querySelectorAll('[data-testid^="mindmap-schema-sidebar-option-layout-"][data-control-type="layoutCard"]'), ); return { triggerVisible: shell instanceof HTMLElement ? shell.dataset.sidebarTriggerVisible === "true" : null, panelOpen: shell instanceof HTMLElement ? shell.dataset.sidebarPanelOpen === "true" : null, railVisible: rail instanceof HTMLElement && rail.getBoundingClientRect().height > 0, drawerVisible: drawer instanceof HTMLElement && drawer.getBoundingClientRect().width > 0, drawerWidth: drawer instanceof HTMLElement ? Math.round(drawer.getBoundingClientRect().width) : 0, title: title instanceof HTMLElement ? (title.textContent || "").trim() : "", closeButton: closeButton instanceof HTMLButtonElement, hideHandle: hideHandle instanceof HTMLButtonElement, restoreHandle: restoreHandle instanceof HTMLButtonElement, structureCardCount: structureCards.length, }; }); } async function verifySidebarStage(page) { const structureTab = page.getByTestId("mindmap-schema-sidebar-tab-structure"); await structureTab.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await structureTab.click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const title = document.querySelector('[data-testid="mindmap-schema-sidebar-title"]'); return ( shell instanceof HTMLElement && shell.dataset.sidebarPanelOpen === "true" && shell.dataset.sidebarActivePanel === "structure" && title instanceof HTMLElement && (title.textContent || "").trim() === "结构" ); }, null, { timeout: UI_TIMEOUT_MS }, ); const opened = await readSidebarMetrics(page); assert(opened.railVisible, "sidebar_rail_missing"); assert(opened.drawerVisible, "sidebar_drawer_not_visible"); assert(opened.drawerWidth >= 280, `sidebar_drawer_width_too_small:${opened.drawerWidth}`); assert(opened.title === "结构", `sidebar_title_unexpected:${opened.title}`); assert(opened.structureCardCount >= 6, `sidebar_structure_cards_missing:${opened.structureCardCount}`); assert(opened.closeButton, "sidebar_close_button_missing"); assert(opened.hideHandle, "sidebar_hide_handle_missing"); await page.getByTestId("mindmap-schema-sidebar-close").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const drawer = document.querySelector('[data-testid="mindmap-schema-sidebar-drawer"]'); return shell instanceof HTMLElement && shell.dataset.sidebarPanelOpen === "false" && !(drawer instanceof HTMLElement); }, null, { timeout: UI_TIMEOUT_MS }, ); const closed = await readSidebarMetrics(page); assert(closed.panelOpen === false, "sidebar_close_not_applied"); await structureTab.click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.sidebarPanelOpen === "true"; }, null, { timeout: UI_TIMEOUT_MS }, ); await page.getByTestId("mindmap-schema-sidebar-hide-handle").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const restoreHandle = document.querySelector('[data-testid="mindmap-schema-sidebar-restore-handle"]'); return ( shell instanceof HTMLElement && shell.dataset.sidebarTriggerVisible === "false" && shell.dataset.sidebarPanelOpen === "false" && restoreHandle instanceof HTMLButtonElement ); }, null, { timeout: UI_TIMEOUT_MS }, ); const hidden = await readSidebarMetrics(page); assert(hidden.restoreHandle, "sidebar_restore_handle_missing"); assert(hidden.triggerVisible === false, "sidebar_trigger_not_hidden"); await page.getByTestId("mindmap-schema-sidebar-restore-handle").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const title = document.querySelector('[data-testid="mindmap-schema-sidebar-title"]'); return ( shell instanceof HTMLElement && shell.dataset.sidebarTriggerVisible === "true" && shell.dataset.sidebarPanelOpen === "true" && title instanceof HTMLElement && (title.textContent || "").trim() === "结构" ); }, null, { timeout: UI_TIMEOUT_MS }, ); const restored = await readSidebarMetrics(page); assert(restored.drawerVisible, "sidebar_drawer_not_restored"); return { opened, closed, hidden, restored }; } async function readRuntimeMindmapState(page) { return page.evaluate(() => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null; const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = mindmapId ? registry[mindmapId] : null; const instance = bridge?.instance || null; const snapshot = bridge?.getSnapshot?.() || null; const data = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) ? snapshot.data || null : null; return { mindmapId, layout: typeof instance?.getLayout === "function" ? instance.getLayout() : null, theme: typeof instance?.getTheme === "function" ? instance.getTheme() : null, rootFillColor: data && typeof data === "object" ? data.fillColor || null : null, commandStatus: document.querySelector('[data-testid="leptos-mindmap-island"]') instanceof HTMLElement ? document.querySelector('[data-testid="leptos-mindmap-island"]').dataset.commandStatus || null : null, }; }); } async function readPersistedMindmapState(page) { return page.evaluate(async () => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null; const match = window.location.pathname.match(/\/documents\/([^/?#]+)/); const documentId = match?.[1] ? decodeURIComponent(match[1]) : null; if (!documentId || !mindmapId) { return { documentId, mindmapId, status: null, layout: null, theme: null, rootFillColor: null, }; } 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); const rootData = payload && typeof payload === "object" && payload.root && typeof payload.root === "object" && !Array.isArray(payload.root) ? payload.root.data || null : null; return { documentId, mindmapId, status: response.status, layout: payload && typeof payload === "object" ? payload.layout || null : null, theme: payload && typeof payload === "object" ? payload.theme || null : null, rootFillColor: rootData && typeof rootData === "object" ? rootData.fillColor || null : null, }; }); } async function waitForMindmapCommandSuccess(page, actionId, predicate) { try { 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 (error) { const debug = await page.evaluate(() => { const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const errorNode = document.querySelector('[data-testid="leptos-mindmap-error"]'); return { commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || null : null, lastSchemaAction: scene instanceof HTMLElement ? scene.dataset.lastSchemaAction || null : null, commandFailedAction: scene instanceof HTMLElement ? scene.dataset.commandFailedAction || null : null, commandFailedMessage: scene instanceof HTMLElement ? scene.dataset.commandFailedMessage || null : null, errorText: errorNode instanceof HTMLElement ? (errorNode.textContent || "").trim() : null, }; }); throw new Error( `mindmap_command_not_success:${actionId}:${JSON.stringify(debug)}:${error instanceof Error ? error.message : String(error)}`, ); } if (predicate) { await page.waitForFunction(predicate, null, { timeout: UI_TIMEOUT_MS }); } } async function verifySidebarActionsStage(page) { await page.getByTestId("mindmap-schema-sidebar-tab-structure").click({ timeout: UI_TIMEOUT_MS }); await page.getByTestId("mindmap-schema-sidebar-option-layout-mind-map").click({ timeout: UI_TIMEOUT_MS }); await waitForMindmapCommandSuccess( page, "setLayout", () => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null; const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = mindmapId ? registry[mindmapId] : null; return bridge?.instance?.getLayout?.() === "mindMap"; }, ); await page.getByTestId("mindmap-schema-sidebar-tab-theme").click({ timeout: UI_TIMEOUT_MS }); await page.getByTestId("mindmap-schema-sidebar-option-theme-dark").click({ timeout: UI_TIMEOUT_MS }); await waitForMindmapCommandSuccess(page, "setTheme"); await page.getByTestId("mindmap-schema-sidebar-tab-nodeStyle").click({ timeout: UI_TIMEOUT_MS }); await page.getByTestId("mindmap-schema-sidebar-option-node-fill-blue").click({ timeout: UI_TIMEOUT_MS }); await waitForMindmapCommandSuccess(page, "painter"); const beforeReload = await readPersistedMindmapState(page); assert(beforeReload.layout === "mindMap", `sidebar_actions_layout_before_reload:${beforeReload.layout}`); assert(beforeReload.theme === "dark", `sidebar_actions_theme_before_reload:${beforeReload.theme}`); assert(beforeReload.rootFillColor === "#dbeafe", `sidebar_actions_fill_before_reload:${beforeReload.rootFillColor}`); await page.reload({ waitUntil: "domcontentloaded" }); await assertMindmapReady(page, "sidebar-actions-reload"); const afterReload = await readPersistedMindmapState(page); assert(afterReload.layout === "mindMap", `sidebar_actions_layout_after_reload:${afterReload.layout}`); assert(afterReload.theme === "dark", `sidebar_actions_theme_after_reload:${afterReload.theme}`); assert(afterReload.rootFillColor === "#dbeafe", `sidebar_actions_fill_after_reload:${afterReload.rootFillColor}`); return { beforeReload, afterReload }; } async function readNavigatorMetrics(page) { return page.evaluate(() => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const navigator = document.querySelector('[data-testid="mindmap-schema-navigator"]'); const searchInput = document.querySelector('[data-testid="mindmap-schema-navigator-search"]'); const minimap = document.querySelector('[data-testid="mindmap-schema-minimap"]'); const readonlyButton = document.querySelector('[data-testid="mindmap-schema-navigator-action-readonly"]'); const zoomInput = document.querySelector('[data-testid="mindmap-schema-navigator-zoom-input"]'); return { navigatorVisible: navigator instanceof HTMLElement && navigator.getBoundingClientRect().height > 0, searchOpen: shell instanceof HTMLElement ? shell.dataset.navigatorSearchOpen === "true" : false, minimapOpen: shell instanceof HTMLElement ? shell.dataset.navigatorMinimapOpen === "true" : false, readonly: shell instanceof HTMLElement ? shell.dataset.navigatorReadonly === "true" : false, searchInputVisible: searchInput instanceof HTMLInputElement && searchInput.getBoundingClientRect().width > 0, minimapVisible: minimap instanceof HTMLElement && minimap.getBoundingClientRect().height > 0, readonlyActive: readonlyButton instanceof HTMLButtonElement ? readonlyButton.dataset.active === "true" : false, zoomValue: zoomInput instanceof HTMLInputElement ? zoomInput.value : null, }; }); } async function verifyNavigatorStage(page, screenshots) { const initial = await readNavigatorMetrics(page); assert(initial.navigatorVisible, "navigator_missing"); assert(!initial.searchInputVisible, "navigator_search_should_be_collapsed"); screenshots.push(await screenshot(page, "07-navigator-icons")); await page.getByTestId("mindmap-schema-navigator-action-search").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const input = document.querySelector('[data-testid="mindmap-schema-navigator-search"]'); return shell instanceof HTMLElement && shell.dataset.navigatorSearchOpen === "true" && input instanceof HTMLInputElement; }, null, { timeout: UI_TIMEOUT_MS }, ); const searchInput = page.getByTestId("mindmap-schema-navigator-search"); await searchInput.fill("主题", { timeout: UI_TIMEOUT_MS }); screenshots.push(await screenshot(page, "08-search-expanded")); await searchInput.press("Escape"); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const input = document.querySelector('[data-testid="mindmap-schema-navigator-search"]'); return shell instanceof HTMLElement && shell.dataset.navigatorSearchOpen === "false" && !(input instanceof HTMLInputElement); }, null, { timeout: UI_TIMEOUT_MS }, ); await page.getByTestId("mindmap-schema-navigator-action-minimap").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const minimap = document.querySelector('[data-testid="mindmap-schema-minimap"]'); return shell instanceof HTMLElement && shell.dataset.navigatorMinimapOpen === "true" && minimap instanceof HTMLElement; }, null, { timeout: UI_TIMEOUT_MS }, ); await page.getByTestId("mindmap-schema-navigator-action-readonly").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.navigatorReadonly === "true"; }, null, { timeout: UI_TIMEOUT_MS }, ); const zoomInputField = page.getByTestId("mindmap-schema-navigator-zoom-input"); const previousZoom = await zoomInputField.inputValue(); await zoomInputField.fill("abc", { timeout: UI_TIMEOUT_MS }); await zoomInputField.press("Enter"); await page.waitForTimeout(200); const afterInvalidZoom = await zoomInputField.inputValue(); assert(afterInvalidZoom === previousZoom, `navigator_zoom_invalid_not_restored:${afterInvalidZoom}`); const final = await readNavigatorMetrics(page); assert(final.minimapVisible, "navigator_minimap_not_visible"); assert(final.readonlyActive, "navigator_readonly_not_active"); return { initial, final }; } async function restoreNavigatorDefaultState(page) { const current = await readNavigatorMetrics(page); if (current.minimapOpen) { await page.getByTestId("mindmap-schema-navigator-action-minimap").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.navigatorMinimapOpen === "false"; }, null, { timeout: UI_TIMEOUT_MS }, ); } if (current.readonly) { await page.getByTestId("mindmap-schema-navigator-action-readonly").click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); return shell instanceof HTMLElement && shell.dataset.navigatorReadonly === "false"; }, null, { timeout: UI_TIMEOUT_MS }, ); } } async function restoreCanvasIdleState(page) { await page.keyboard.press("Escape").catch(() => undefined); const root = page.getByTestId("mnote-mindmap-editor-root"); const box = await root.boundingBox(); if (box && box.width > 0 && box.height > 0) { await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); } await page.waitForTimeout(200); } async function main() { if (!["all", "toolbar", "fullscreen", "chrome-hide", "sidebar", "sidebar-actions", "navigator", "context-menu", "theme"].includes(STAGE)) { throw new Error(`当前 smoke 只实现 --stage all/toolbar/fullscreen/chrome-hide/sidebar/sidebar-actions/navigator/context-menu/theme,收到:${STAGE}`); } await fs.mkdir(OUTPUT_DIR, { recursive: true }); 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 screenshots = []; const createdIds = []; const result = { ok: false, task: TASK, stage: STAGE, baseUrl: BASE_URL, documentId: null, mindmapId: null, toolbar: null, screenshots, }; try { await ensureAuthenticated(page, context.request); const doc = await createTempDocument(context.request, null); createdIds.push(doc.documentId); const title = `task167-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); await assertMindmapReady(page, STAGE); result.mindmapId = (await readMindmapIdentity(page)).mindmapId; if (STAGE === "all") { result.toolbar = await verifyToolbarStage(page); screenshots.push(await screenshot(page, "01-toolbar-single-row")); result.fullscreen = await verifyFullscreenStage(page); screenshots.push(await screenshot(page, "02-fullscreen-canvas")); await restoreCanvasIdleState(page); result.chromeHide = await verifyChromeHideStage(page); screenshots.push(await screenshot(page, "03-chrome-hidden-after-leave")); screenshots.push(await screenshot(page, "04-chrome-restored-after-click")); result.sidebar = await verifySidebarStage(page); screenshots.push(await screenshot(page, "05-sidebar-structure-drawer")); await page.getByTestId("mindmap-schema-sidebar-hide-handle").click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined); await page.waitForTimeout(200); screenshots.push(await screenshot(page, "06-sidebar-hidden-handle")); await page.getByTestId("mindmap-schema-sidebar-restore-handle").click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined); await page.waitForTimeout(200); await page.getByTestId("mindmap-schema-sidebar-close").click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined); await page.waitForTimeout(200); result.navigator = await verifyNavigatorStage(page, screenshots); await restoreNavigatorDefaultState(page); result.contextMenu = await verifyContextMenuStage(page); screenshots.push(await screenshot(page, "09-node-context-menu")); await restoreCanvasIdleState(page); result.theme = await verifyThemeStage(page); screenshots.push(await screenshot(page, "10-kmind-theme-baseline")); result.sidebarActions = await verifySidebarActionsStage(page); result.toolbarRows = result.toolbar?.desktop?.toolbarRows ?? null; result.fullscreenOk = Boolean(result.fullscreen); result.chromeHideOk = Boolean(result.chromeHide); result.sidebarDrawerOk = Boolean(result.sidebar); result.navigatorOk = Boolean(result.navigator); result.contextMenuOk = Boolean(result.contextMenu); result.themeOk = Boolean(result.theme); result.reloadOk = Boolean(result.sidebarActions); } else if (STAGE === "toolbar") { result.toolbar = await verifyToolbarStage(page); screenshots.push(await screenshot(page, "01-toolbar-single-row")); } else if (STAGE === "fullscreen") { result.fullscreen = await verifyFullscreenStage(page); screenshots.push(await screenshot(page, "02-fullscreen-canvas")); } else if (STAGE === "chrome-hide") { result.chromeHide = await verifyChromeHideStage(page); screenshots.push(await screenshot(page, "03-chrome-hidden-after-leave")); screenshots.push(await screenshot(page, "04-chrome-restored-after-click")); } else if (STAGE === "sidebar") { result.sidebar = await verifySidebarStage(page); screenshots.push(await screenshot(page, "05-sidebar-structure-drawer")); await page.getByTestId("mindmap-schema-sidebar-hide-handle").click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined); await page.waitForTimeout(200); screenshots.push(await screenshot(page, "06-sidebar-hidden-handle")); } else if (STAGE === "sidebar-actions") { result.sidebarActions = await verifySidebarActionsStage(page); } else if (STAGE === "navigator") { result.navigator = await verifyNavigatorStage(page, screenshots); } else if (STAGE === "context-menu") { result.contextMenu = await verifyContextMenuStage(page); screenshots.push(await screenshot(page, "09-node-context-menu")); } else if (STAGE === "theme") { result.theme = await verifyThemeStage(page); screenshots.push(await screenshot(page, "10-kmind-theme-baseline")); } result.ok = true; await writeResult(result); } catch (error) { result.error = error instanceof Error ? error.stack || error.message : String(error); screenshots.push(await screenshot(page, "99-failure").catch(() => null)); await writeResult(result); throw error; } finally { await cleanupDocuments(context.request, createdIds).catch(() => undefined); await browser.close(); } } main().catch((error) => { console.error(error); process.exitCode = 1; });