1043 lines
50 KiB
JavaScript
1043 lines
50 KiB
JavaScript
#!/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;
|
|
});
|