Files

1043 lines
50 KiB
JavaScript
Raw Permalink Normal View History

2026-05-13 22:43:16 +08:00
#!/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;
});