feat: 收口 mindmap Phase 6 Leptos UI shell
This commit is contained in:
@@ -0,0 +1,978 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
assert,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
openDocument,
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
const TASK = "task166-mindmap-phase6-block-smoke";
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
|
||||
function checkPort(port) {
|
||||
const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
const lines = (result.stdout || "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const rows = lines.slice(1).map((line) => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pid = parts[1] || "";
|
||||
const commandLine =
|
||||
pid.length > 0
|
||||
? spawnSync("ps", ["-p", pid, "-o", "command="], {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
}).stdout.trim()
|
||||
: "";
|
||||
return {
|
||||
command: parts[0] || "",
|
||||
pid,
|
||||
commandLine,
|
||||
name: parts.slice(8).join(" "),
|
||||
raw: line,
|
||||
};
|
||||
});
|
||||
return { port, rows };
|
||||
}
|
||||
|
||||
async function writeResult(payload) {
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...payload, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function screenshot(page, name) {
|
||||
const file = path.join(OUTPUT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: file, fullPage: true });
|
||||
return file;
|
||||
}
|
||||
|
||||
async function readMindmapMetrics(page) {
|
||||
return page.evaluate(() => {
|
||||
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
|
||||
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
|
||||
const scene = root instanceof HTMLElement ? root.querySelector('[data-testid="leptos-mindmap-island"]') : null;
|
||||
const overlayLayer = root instanceof HTMLElement ? root.querySelector('[data-testid="mindmap-overlay-layer"]') : null;
|
||||
const canvasLayer = root instanceof HTMLElement ? root.querySelector('[data-testid="mindmap-canvas-layer"]') : null;
|
||||
const schemaToolbar = document.querySelector('[data-testid="mindmap-schema-toolbar"]');
|
||||
const schemaSidebar = document.querySelector('[data-testid="mindmap-schema-sidebar"]');
|
||||
const schemaNavigator = document.querySelector('[data-testid="mindmap-schema-navigator"]');
|
||||
const schemaCount = document.querySelector('[data-testid="mindmap-schema-count"]');
|
||||
const schemaContextMenu = document.querySelector('[data-testid="mindmap-schema-context-menu"]');
|
||||
const rustShellMount = document.querySelector('[data-testid="mindmap-rust-shell-mount"]');
|
||||
const rustShell = document.querySelector('[data-testid="mindmap-rust-shell"]');
|
||||
const error = document.querySelector('[data-testid="leptos-mindmap-error"]');
|
||||
const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null;
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = mindmapId ? registry[mindmapId] : null;
|
||||
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
|
||||
const canonicalRoot =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
const snapshotStats = { nodeCount: 0, edgeCount: 0, nodeTexts: [] };
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
||||
snapshotStats.nodeCount += 1;
|
||||
const text = node.data && typeof node.data.text === "string" ? node.data.text.trim() : "";
|
||||
if (text) snapshotStats.nodeTexts.push(text);
|
||||
const children = Array.isArray(node.children) ? node.children : [];
|
||||
snapshotStats.edgeCount += children.length;
|
||||
children.forEach(visit);
|
||||
};
|
||||
visit(canonicalRoot);
|
||||
const runtimeNodes = runtime
|
||||
? Array.from(runtime.querySelectorAll(".smm-node")).filter((node) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}).length
|
||||
: 0;
|
||||
const svgPaths = runtime
|
||||
? Array.from(runtime.querySelectorAll("svg path, svg line, svg polyline")).filter((edge) => {
|
||||
const rect = edge.getBoundingClientRect();
|
||||
return rect.width > 0 || rect.height > 0 || edge.getAttribute("d") || edge.getAttribute("points");
|
||||
}).length
|
||||
: 0;
|
||||
const runtimeRect = runtime?.getBoundingClientRect();
|
||||
const rootRect = root?.getBoundingClientRect();
|
||||
const overlayRect = overlayLayer?.getBoundingClientRect();
|
||||
const canvasRect = canvasLayer?.getBoundingClientRect();
|
||||
return {
|
||||
errorStage: error instanceof HTMLElement ? error.dataset.stage || "unknown_error" : null,
|
||||
rootExists: root instanceof HTMLElement,
|
||||
mindmapId,
|
||||
runtimeExists: runtime instanceof HTMLElement,
|
||||
runtimeEngine: runtime instanceof HTMLElement ? runtime.dataset.runtimeEngine || null : null,
|
||||
runtimeReady: runtime instanceof HTMLElement ? runtime.dataset.runtimeReady === "true" : false,
|
||||
lastViewEvent:
|
||||
root instanceof HTMLElement
|
||||
? root.querySelector('[data-testid="leptos-mindmap-island"]')?.dataset.lastViewEvent || null
|
||||
: null,
|
||||
debugChrome:
|
||||
scene instanceof HTMLElement ? scene.dataset.debugChrome || null : null,
|
||||
uiShell:
|
||||
scene instanceof HTMLElement ? scene.dataset.uiShell || null : null,
|
||||
rustShell: {
|
||||
mount: rustShellMount instanceof HTMLElement,
|
||||
mountSource: rustShellMount instanceof HTMLElement ? rustShellMount.dataset.uiShellSource || null : null,
|
||||
shell: rustShell instanceof HTMLElement,
|
||||
shellSource: rustShell instanceof HTMLElement ? rustShell.dataset.uiShellSource || null : null,
|
||||
},
|
||||
schemaChrome: {
|
||||
toolbar:
|
||||
schemaToolbar instanceof HTMLElement &&
|
||||
schemaToolbar.dataset.schemaSource === "mindmapDefaultUiSchema" &&
|
||||
schemaToolbar.dataset.uiSource === "leptos-rust-shell",
|
||||
sidebar:
|
||||
schemaSidebar instanceof HTMLElement &&
|
||||
schemaSidebar.dataset.schemaSource === "mindmapDefaultUiSchema" &&
|
||||
schemaSidebar.dataset.uiSource === "leptos-rust-shell",
|
||||
navigator:
|
||||
schemaNavigator instanceof HTMLElement &&
|
||||
schemaNavigator.dataset.schemaSource === "mindmapDefaultUiSchema" &&
|
||||
schemaNavigator.dataset.uiSource === "leptos-rust-shell",
|
||||
count:
|
||||
schemaCount instanceof HTMLElement &&
|
||||
schemaCount.dataset.schemaSource === "mindmapDefaultUiSchema" &&
|
||||
schemaCount.dataset.uiSource === "leptos-rust-shell",
|
||||
toolbarActions: Array.from(document.querySelectorAll("[data-mindmap-action-id]"))
|
||||
.map((item) => item instanceof HTMLElement ? item.dataset.mindmapActionId : null)
|
||||
.filter(Boolean),
|
||||
disabledActions: Array.from(document.querySelectorAll("[data-mindmap-action-id]"))
|
||||
.filter((item) => item instanceof HTMLButtonElement && item.disabled)
|
||||
.map((item) => item.dataset.mindmapActionId)
|
||||
.filter(Boolean),
|
||||
sidebarPanels: Array.from(document.querySelectorAll("[data-mindmap-sidebar-panel-id]"))
|
||||
.map((item) => item instanceof HTMLElement ? item.dataset.mindmapSidebarPanelId : null)
|
||||
.filter(Boolean),
|
||||
statsText: schemaNavigator?.querySelector('[data-testid="mindmap-schema-navigator-stats"]')?.textContent || null,
|
||||
countText: schemaCount?.textContent || null,
|
||||
zoomText: schemaNavigator?.querySelector('[data-testid="mindmap-schema-navigator-zoom"]')?.textContent || null,
|
||||
minimap: Boolean(document.querySelector('[data-testid="mindmap-schema-minimap"]')),
|
||||
contextMenu:
|
||||
schemaContextMenu instanceof HTMLElement
|
||||
? {
|
||||
kind: schemaContextMenu.dataset.contextMenuKind || null,
|
||||
actions: Array.from(schemaContextMenu.querySelectorAll("[data-mindmap-context-action-id]"))
|
||||
.map((item) => item instanceof HTMLElement ? item.dataset.mindmapContextActionId : null)
|
||||
.filter(Boolean),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
overlayShell: {
|
||||
overlay: overlayLayer instanceof HTMLElement,
|
||||
canvas: canvasLayer instanceof HTMLElement,
|
||||
rootBox: rootRect
|
||||
? { width: rootRect.width, height: rootRect.height }
|
||||
: { width: 0, height: 0 },
|
||||
overlayBox: overlayRect
|
||||
? { width: overlayRect.width, height: overlayRect.height }
|
||||
: { width: 0, height: 0 },
|
||||
canvasBox: canvasRect
|
||||
? { width: canvasRect.width, height: canvasRect.height }
|
||||
: { width: 0, height: 0 },
|
||||
},
|
||||
legacyFallback: {
|
||||
toolbar: Boolean(document.querySelector('[data-testid="mindmap-command-toolbar"]')),
|
||||
sidebar: Boolean(document.querySelector('[data-testid="mindmap-sidebar"]')),
|
||||
bottomBar: Boolean(document.querySelector('[data-testid="mindmap-bottom-bar"]')),
|
||||
},
|
||||
viewCommandStatus:
|
||||
scene instanceof HTMLElement ? scene.dataset.viewCommandStatus || null : null,
|
||||
bridgeExists: Boolean(bridge),
|
||||
runtimeBox: runtimeRect
|
||||
? { width: runtimeRect.width, height: runtimeRect.height }
|
||||
: { width: 0, height: 0 },
|
||||
nodeCount: snapshotStats.nodeCount || runtimeNodes,
|
||||
edgeCount: snapshotStats.edgeCount || svgPaths,
|
||||
runtimeNodeCount: runtimeNodes,
|
||||
svgPathCount: svgPaths,
|
||||
nodeTexts: snapshotStats.nodeTexts,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function assertMindmapReady(page, stage) {
|
||||
await page.locator('[data-testid="mnote-mindmap-editor-root"]').waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator('[data-testid="simple-mind-map-runtime"]').waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
|
||||
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
|
||||
const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null;
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = mindmapId ? registry[mindmapId] : null;
|
||||
return (
|
||||
runtime instanceof HTMLElement &&
|
||||
runtime.dataset.runtimeEngine === "simple-mind-map" &&
|
||||
runtime.dataset.runtimeReady === "true" &&
|
||||
Boolean(bridge)
|
||||
);
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const metrics = await readMindmapMetrics(page);
|
||||
if (metrics.errorStage) {
|
||||
throw new Error(`${metrics.errorStage}: ${stage}`);
|
||||
}
|
||||
assert(metrics.rootExists, `${stage}: mnote-mindmap-editor-root 不存在`);
|
||||
assert(metrics.runtimeExists, `${stage}: runtime_render_failed`);
|
||||
assert(metrics.runtimeEngine === "simple-mind-map", `${stage}: runtime_engine_failed`);
|
||||
assert(metrics.runtimeReady, `${stage}: runtime_not_ready`);
|
||||
assert(metrics.bridgeExists, `${stage}: bridge_not_registered`);
|
||||
assert(metrics.runtimeBox.width > 0 && metrics.runtimeBox.height > 0, `${stage}: runtime_render_failed`);
|
||||
assert(metrics.debugChrome === "false", `${stage}: debug_chrome_unexpected`);
|
||||
assert(metrics.uiShell === "leptos-rust-shell", `${stage}: ui_shell_source_unexpected`);
|
||||
assert(metrics.rustShell.mount, `${stage}: rust_shell_mount_missing`);
|
||||
assert(metrics.rustShell.mountSource === "leptos-rust-shell", `${stage}: rust_shell_mount_source_unexpected`);
|
||||
assert(metrics.rustShell.shell, `${stage}: rust_shell_missing`);
|
||||
assert(metrics.rustShell.shellSource === "leptos-rust-shell", `${stage}: rust_shell_source_unexpected`);
|
||||
assert(metrics.schemaChrome.toolbar, `${stage}: ui_shell_missing toolbar`);
|
||||
assert(metrics.schemaChrome.sidebar, `${stage}: ui_shell_missing sidebar`);
|
||||
assert(metrics.schemaChrome.navigator, `${stage}: ui_shell_missing navigator`);
|
||||
assert(metrics.schemaChrome.count, `${stage}: ui_shell_missing count`);
|
||||
assert(metrics.overlayShell.overlay, `${stage}: floating_overlay_missing overlay`);
|
||||
assert(metrics.overlayShell.canvas, `${stage}: floating_overlay_missing canvas`);
|
||||
assert(metrics.runtimeBox.width / Math.max(metrics.overlayShell.rootBox.width, 1) >= 0.9, `${stage}: runtime_width_squeezed`);
|
||||
assert(metrics.runtimeBox.height / Math.max(metrics.overlayShell.rootBox.height, 1) >= 0.85, `${stage}: runtime_height_squeezed`);
|
||||
assert(!metrics.legacyFallback.toolbar, `${stage}: legacy_toolbar_unexpected`);
|
||||
assert(!metrics.legacyFallback.sidebar, `${stage}: legacy_sidebar_unexpected`);
|
||||
assert(!metrics.legacyFallback.bottomBar, `${stage}: legacy_bottom_bar_unexpected`);
|
||||
const expectedActions = [
|
||||
"undo",
|
||||
"redo",
|
||||
"insertSiblingAfter",
|
||||
"insertChild",
|
||||
"deleteNode",
|
||||
"tag",
|
||||
"hyperlink",
|
||||
"note",
|
||||
"image",
|
||||
"icon",
|
||||
"summary",
|
||||
"associativeLine",
|
||||
"formula",
|
||||
"painter",
|
||||
"import",
|
||||
"export",
|
||||
];
|
||||
for (const actionId of expectedActions) {
|
||||
assert(metrics.schemaChrome.toolbarActions.includes(actionId), `${stage}: ui_shell_missing action=${actionId}`);
|
||||
}
|
||||
for (const panelId of ["nodeStyle", "baseStyle", "theme", "structure", "outline"]) {
|
||||
assert(metrics.schemaChrome.sidebarPanels.includes(panelId), `${stage}: ui_shell_missing panel=${panelId}`);
|
||||
}
|
||||
assert(/节点/.test(metrics.schemaChrome.countText || ""), `${stage}: ui_shell_missing count_stats`);
|
||||
assert(/%/.test(metrics.schemaChrome.zoomText || ""), `${stage}: ui_shell_missing zoom`);
|
||||
assert(metrics.nodeCount >= 4, `${stage}: runtime_render_failed nodeCount=${metrics.nodeCount}`);
|
||||
assert(metrics.edgeCount >= 3, `${stage}: runtime_render_failed edgeCount=${metrics.edgeCount}`);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
async function readMindmapSnapshotStats(page, mindmapId) {
|
||||
return page.evaluate((mindmapId) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
|
||||
const root =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
const stats = { nodeCount: 0, edgeCount: 0, nodeTexts: [] };
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
||||
stats.nodeCount += 1;
|
||||
const text = node.data && typeof node.data.text === "string" ? node.data.text.trim() : "";
|
||||
if (text) stats.nodeTexts.push(text);
|
||||
const children = Array.isArray(node.children) ? node.children : [];
|
||||
stats.edgeCount += children.length;
|
||||
children.forEach(visit);
|
||||
};
|
||||
visit(root);
|
||||
return stats;
|
||||
}, mindmapId);
|
||||
}
|
||||
|
||||
async function clickToolbarActionAndWaitForCommand(page, actionId) {
|
||||
await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
scene.dataset.commandStatus = "smoke-waiting";
|
||||
delete scene.dataset.lastSchemaAction;
|
||||
}
|
||||
});
|
||||
await page.getByTestId(`mindmap-schema-toolbar-action-${actionId}`).click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
(actionId) => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return (
|
||||
scene instanceof HTMLElement &&
|
||||
scene.dataset.lastSchemaAction === actionId &&
|
||||
scene.dataset.commandStatus === "success"
|
||||
);
|
||||
},
|
||||
actionId,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
).catch(async (error) => {
|
||||
const diagnostics = await page.evaluate((actionId) => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
const button = document.querySelector(`[data-testid="mindmap-schema-toolbar-action-${actionId}"]`);
|
||||
const errorLayer = document.querySelector('[data-testid="leptos-mindmap-error"]');
|
||||
return {
|
||||
sceneDataset: scene instanceof HTMLElement ? { ...scene.dataset } : null,
|
||||
buttonDisabled: button instanceof HTMLButtonElement ? button.disabled : null,
|
||||
buttonExists: button instanceof HTMLElement,
|
||||
errorStage: errorLayer instanceof HTMLElement ? errorLayer.getAttribute("data-stage") : null,
|
||||
errorText: errorLayer instanceof HTMLElement ? errorLayer.textContent : null,
|
||||
};
|
||||
}, actionId);
|
||||
throw new Error(`command_failed:${actionId}:command_status_timeout:${JSON.stringify(diagnostics)}:${error.message}`);
|
||||
});
|
||||
return { ok: () => true };
|
||||
}
|
||||
|
||||
async function clickSidebarOptionAndWaitForCommand(page, optionId, actionId) {
|
||||
await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
scene.dataset.commandStatus = "smoke-waiting";
|
||||
delete scene.dataset.lastSchemaAction;
|
||||
delete scene.dataset.lastSchemaCommand;
|
||||
}
|
||||
});
|
||||
await page.getByTestId(`mindmap-schema-sidebar-option-${optionId}`).click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
(actionId) => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return (
|
||||
scene instanceof HTMLElement &&
|
||||
scene.dataset.lastSchemaAction === actionId &&
|
||||
scene.dataset.commandStatus === "success"
|
||||
);
|
||||
},
|
||||
actionId,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
).catch(async (error) => {
|
||||
const diagnostics = await page.evaluate((optionId) => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
const option = document.querySelector(`[data-testid="mindmap-schema-sidebar-option-${optionId}"]`);
|
||||
const errorLayer = document.querySelector('[data-testid="leptos-mindmap-error"]');
|
||||
return {
|
||||
sceneDataset: scene instanceof HTMLElement ? { ...scene.dataset } : null,
|
||||
optionExists: option instanceof HTMLElement,
|
||||
errorStage: errorLayer instanceof HTMLElement ? errorLayer.getAttribute("data-stage") : null,
|
||||
errorText: errorLayer instanceof HTMLElement ? errorLayer.textContent : null,
|
||||
};
|
||||
}, optionId);
|
||||
throw new Error(`command_failed:${actionId}:${optionId}:command_status_timeout:${JSON.stringify(diagnostics)}:${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyReadonlyToolbarState(page, stage) {
|
||||
await page.getByTestId("mindmap-schema-navigator-action-readonly").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const insertChild = document.querySelector('[data-testid="mindmap-schema-toolbar-action-insertChild"]');
|
||||
const deleteNode = document.querySelector('[data-testid="mindmap-schema-toolbar-action-deleteNode"]');
|
||||
const zoomIn = document.querySelector('[data-testid="mindmap-schema-navigator-action-zoomIn"]');
|
||||
const centerRoot = document.querySelector('[data-testid="mindmap-schema-navigator-action-centerRoot"]');
|
||||
return (
|
||||
insertChild instanceof HTMLButtonElement &&
|
||||
deleteNode instanceof HTMLButtonElement &&
|
||||
zoomIn instanceof HTMLButtonElement &&
|
||||
centerRoot instanceof HTMLButtonElement &&
|
||||
insertChild.disabled &&
|
||||
deleteNode.disabled &&
|
||||
!zoomIn.disabled &&
|
||||
!centerRoot.disabled
|
||||
);
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
).catch((error) => {
|
||||
throw new Error(`ui_shell_missing:${stage}:readonly_disabled_state:${error.message}`);
|
||||
});
|
||||
await page.getByTestId("mindmap-schema-navigator-action-readonly").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const insertChild = document.querySelector('[data-testid="mindmap-schema-toolbar-action-insertChild"]');
|
||||
return insertChild instanceof HTMLButtonElement && !insertChild.disabled;
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function exerciseToolbarNodeActions(page, documentId, mindmapId) {
|
||||
const before = await readMindmapSnapshotStats(page, mindmapId);
|
||||
const childResponse = await clickToolbarActionAndWaitForCommand(page, "insertChild");
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, expected }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
|
||||
const root =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
let count = 0;
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
||||
count += 1;
|
||||
if (Array.isArray(node.children)) node.children.forEach(visit);
|
||||
};
|
||||
visit(root);
|
||||
return count >= expected;
|
||||
},
|
||||
{ mindmapId, expected: before.nodeCount + 1 },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const afterChild = await readMindmapSnapshotStats(page, mindmapId);
|
||||
|
||||
const siblingResponse = await clickToolbarActionAndWaitForCommand(page, "insertSiblingAfter");
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, expected }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
|
||||
const root =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
let count = 0;
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
||||
count += 1;
|
||||
if (Array.isArray(node.children)) node.children.forEach(visit);
|
||||
};
|
||||
visit(root);
|
||||
return count >= expected;
|
||||
},
|
||||
{ mindmapId, expected: afterChild.nodeCount + 1 },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const afterSibling = await readMindmapSnapshotStats(page, mindmapId);
|
||||
|
||||
const deleteResponse = await clickToolbarActionAndWaitForCommand(page, "deleteNode");
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, expectedMax }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
|
||||
const root =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
let count = 0;
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
||||
count += 1;
|
||||
if (Array.isArray(node.children)) node.children.forEach(visit);
|
||||
};
|
||||
visit(root);
|
||||
return count <= expectedMax;
|
||||
},
|
||||
{ mindmapId, expectedMax: afterSibling.nodeCount - 1 },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const afterDelete = await readMindmapSnapshotStats(page, mindmapId);
|
||||
|
||||
return {
|
||||
insertChildOk: childResponse.ok() && afterChild.nodeCount >= before.nodeCount + 1,
|
||||
insertSiblingOk: siblingResponse.ok() && afterSibling.nodeCount >= afterChild.nodeCount + 1,
|
||||
deleteOk: deleteResponse.ok() && afterDelete.nodeCount <= afterSibling.nodeCount - 1,
|
||||
beforeNodeCount: before.nodeCount,
|
||||
afterChildNodeCount: afterChild.nodeCount,
|
||||
afterSiblingNodeCount: afterSibling.nodeCount,
|
||||
afterDeleteNodeCount: afterDelete.nodeCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function exerciseSidebarActions(page, documentId, mindmapId) {
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-theme").click({ timeout: UI_TIMEOUT_MS });
|
||||
await clickSidebarOptionAndWaitForCommand(page, "theme-classic4", "setTheme");
|
||||
const themedProjection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
if (themedProjection?.theme !== "classic4") {
|
||||
throw new Error(`command_failed:setTheme theme=${JSON.stringify(themedProjection?.theme)}`);
|
||||
}
|
||||
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-structure").click({ timeout: UI_TIMEOUT_MS });
|
||||
await clickSidebarOptionAndWaitForCommand(page, "layout-mind-map", "setLayout");
|
||||
const layoutProjection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
if (layoutProjection?.layout !== "mindMap") {
|
||||
throw new Error(`command_failed:setLayout layout=${JSON.stringify(layoutProjection?.layout)}`);
|
||||
}
|
||||
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-nodeStyle").click({ timeout: UI_TIMEOUT_MS });
|
||||
await clickSidebarOptionAndWaitForCommand(page, "node-fill-blue", "painter");
|
||||
const nodeStyleProjection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
const rootFill = nodeStyleProjection?.root?.data?.fillColor;
|
||||
if (rootFill !== "#dbeafe") {
|
||||
throw new Error(`command_failed:nodeStyle compat fill=${JSON.stringify(rootFill)}`);
|
||||
}
|
||||
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-baseStyle").click({ timeout: UI_TIMEOUT_MS });
|
||||
await clickSidebarOptionAndWaitForCommand(page, "base-curve-line", "painter");
|
||||
const baseStyleProjection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
const lineStyle = baseStyleProjection?.compatPayload?.style?.map?.lineStyle;
|
||||
if (lineStyle !== "curve") {
|
||||
throw new Error(`command_failed:baseStyle compat lineStyle=${JSON.stringify(lineStyle)}`);
|
||||
}
|
||||
|
||||
await page.getByTestId("mindmap-schema-sidebar-tab-outline").click({ timeout: UI_TIMEOUT_MS });
|
||||
const outlineItems = await page
|
||||
.locator('[data-testid="mindmap-schema-sidebar-outline-outline"] li')
|
||||
.evaluateAll((items) => items.map((item) => item.textContent?.trim()).filter(Boolean));
|
||||
if (outlineItems.length === 0) {
|
||||
throw new Error("ui_shell_missing:outline_items");
|
||||
}
|
||||
|
||||
return {
|
||||
themeOk: themedProjection.theme === "classic4",
|
||||
layoutOk: layoutProjection.layout === "mindMap",
|
||||
nodeStyleCompatOk: rootFill === "#dbeafe",
|
||||
baseStyleCompatOk: lineStyle === "curve",
|
||||
outlineDerivedOk: outlineItems.length > 0,
|
||||
outlineItems,
|
||||
};
|
||||
}
|
||||
|
||||
async function insertMindmapThroughSlash(page) {
|
||||
const editor = page
|
||||
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
||||
.first();
|
||||
await editor.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await editor.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.type("/");
|
||||
const item = page.getByTestId("slash-item-mindmap").first();
|
||||
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await item.click({ timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
async function fetchAdapterProjection(page, documentId, mindmapId) {
|
||||
return page.evaluate(
|
||||
async ({ documentId, mindmapId }) => {
|
||||
const response = await fetch(
|
||||
`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`,
|
||||
{ headers: { Accept: "application/json" } },
|
||||
);
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(`projection_load_failed:${response.status}`);
|
||||
}
|
||||
if (payload && typeof payload === "object" && !Array.isArray(payload) && payload.result) {
|
||||
return payload.result;
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
{ documentId, mindmapId },
|
||||
);
|
||||
}
|
||||
|
||||
async function editRootNode(page, documentId, mindmapId, text) {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`) &&
|
||||
response.request().method() === "POST",
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.evaluate(
|
||||
async ({ documentId, mindmapId, text }) => {
|
||||
const response = await fetch(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
commandName: "mindmap.command.apply",
|
||||
documentId,
|
||||
mindmapId,
|
||||
commands: [{ type: "updateText", mindmapId, nodeId: "root", text }],
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`command_failed:${response.status}`);
|
||||
}
|
||||
},
|
||||
{ documentId, mindmapId, text },
|
||||
);
|
||||
const response = await responsePromise;
|
||||
if (!response.ok()) {
|
||||
throw new Error(`command_failed:${response.status()}`);
|
||||
}
|
||||
const projection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
const root = projection && typeof projection === "object" && projection.root ? projection.root : projection;
|
||||
const rootText = root?.data?.text;
|
||||
if (rootText !== text) {
|
||||
throw new Error(`command_failed:update_text_projection_mismatch text=${JSON.stringify(rootText)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function zoomMindmapAndVerifyViewPatch(page, documentId, mindmapId) {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`) &&
|
||||
response.request().method() === "POST" &&
|
||||
response.request().postData()?.includes('"type":"patchView"'),
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.getByTestId("mindmap-schema-navigator-action-zoomIn").click({ timeout: UI_TIMEOUT_MS });
|
||||
const response = await responsePromise;
|
||||
if (!response.ok()) {
|
||||
throw new Error(`view_command_failed:${response.status()}`);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement && scene.dataset.viewCommandStatus === "success";
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const projection = await fetchAdapterProjection(page, documentId, mindmapId);
|
||||
const scale = projection?.view?.state?.scale;
|
||||
if (!(typeof scale === "number" && scale > 1)) {
|
||||
const requestPayload = JSON.parse(response.request().postData() || "null");
|
||||
throw new Error(
|
||||
`view_patch_failed scale=${scale} view=${JSON.stringify(projection?.view ?? null)} patch=${JSON.stringify(
|
||||
requestPayload?.commands?.[0]?.patch ?? null,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
async function centerRootAndVerifyViewPatch(page, documentId, mindmapId) {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`) &&
|
||||
response.request().method() === "POST" &&
|
||||
response.request().postData()?.includes('"type":"patchView"'),
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.getByTestId("mindmap-schema-navigator-action-centerRoot").click({ timeout: UI_TIMEOUT_MS });
|
||||
const response = await responsePromise;
|
||||
if (!response.ok()) {
|
||||
throw new Error(`view_command_failed:centerRoot:${response.status()}`);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement && scene.dataset.viewCommandStatus === "success";
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function exerciseNavigatorActions(page, documentId, mindmapId) {
|
||||
const centerRootOk = await centerRootAndVerifyViewPatch(page, documentId, mindmapId);
|
||||
const zoomScale = await zoomMindmapAndVerifyViewPatch(page, documentId, mindmapId);
|
||||
await page.getByTestId("mindmap-schema-navigator-search").fill("KMIND", { timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement && scene.dataset.searchStatus === "ready" && scene.dataset.lastSearchQuery === "KMIND";
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
await page.getByTestId("mindmap-schema-navigator-action-minimap").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.getByTestId("mindmap-schema-minimap").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
return {
|
||||
centerRootOk,
|
||||
zoomOk: typeof zoomScale === "number" && zoomScale > 1,
|
||||
searchOk: true,
|
||||
minimapOk: true,
|
||||
zoomScale,
|
||||
};
|
||||
}
|
||||
|
||||
async function openContextMenu(page, targetKind) {
|
||||
if (targetKind === "node") {
|
||||
const node = page.locator('[data-testid="simple-mind-map-runtime"] .smm-node').first();
|
||||
await node.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await node.click({ timeout: UI_TIMEOUT_MS });
|
||||
await node.click({ button: "right", timeout: UI_TIMEOUT_MS });
|
||||
} else {
|
||||
await page.getByTestId("simple-mind-map-runtime").click({ button: "right", position: { x: 36, y: 36 }, timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
await page.getByTestId("mindmap-schema-context-menu").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const metrics = await readMindmapMetrics(page);
|
||||
assert(metrics.schemaChrome.contextMenu?.kind === targetKind, `ui_shell_missing:context_menu_kind expected=${targetKind} actual=${metrics.schemaChrome.contextMenu?.kind}`);
|
||||
return metrics.schemaChrome.contextMenu.actions;
|
||||
}
|
||||
|
||||
async function exerciseContextMenuActions(page, mindmapId) {
|
||||
const nodeActions = await openContextMenu(page, "node");
|
||||
for (const actionId of ["insertChild", "insertSiblingAfter", "deleteNode", "summary", "associativeLine", "expandCollapse", "copyNodeText"]) {
|
||||
assert(nodeActions.includes(actionId), `ui_shell_missing:node_context_action=${actionId}`);
|
||||
}
|
||||
await page.getByTestId("mindmap-schema-context-menu-action-copyNodeText").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement && scene.dataset.lastContextMenuAction === "copyNodeText" && (scene.dataset.copiedNodeText || "").length > 0;
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const before = await readMindmapSnapshotStats(page, mindmapId);
|
||||
await openContextMenu(page, "node");
|
||||
await page.evaluate(() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
if (scene instanceof HTMLElement) {
|
||||
scene.dataset.commandStatus = "smoke-waiting";
|
||||
delete scene.dataset.lastSchemaAction;
|
||||
}
|
||||
});
|
||||
await page.getByTestId("mindmap-schema-context-menu-action-insertChild").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
|
||||
return scene instanceof HTMLElement && scene.dataset.lastSchemaAction === "insertChild" && scene.dataset.commandStatus === "success";
|
||||
},
|
||||
null,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
const afterInsert = await readMindmapSnapshotStats(page, mindmapId);
|
||||
|
||||
const canvasActions = await openContextMenu(page, "canvas");
|
||||
for (const actionId of ["centerRoot", "fitView", "search", "readonly"]) {
|
||||
assert(canvasActions.includes(actionId), `ui_shell_missing:canvas_context_action=${actionId}`);
|
||||
}
|
||||
assert(!canvasActions.includes("insertChild"), "ui_shell_missing:canvas_context_hides_node_action");
|
||||
|
||||
return {
|
||||
nodeMenuOk: nodeActions.includes("insertChild") && nodeActions.includes("copyNodeText"),
|
||||
canvasMenuOk: canvasActions.includes("centerRoot") && canvasActions.includes("readonly") && !canvasActions.includes("insertChild"),
|
||||
copyTextOk: true,
|
||||
insertChildOk: afterInsert.nodeCount >= before.nodeCount + 1,
|
||||
beforeNodeCount: before.nodeCount,
|
||||
afterInsertNodeCount: afterInsert.nodeCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForMindmapText(page, mindmapId, text, stage) {
|
||||
await page.waitForFunction(
|
||||
({ mindmapId, text }) => {
|
||||
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
|
||||
const bridge = registry[mindmapId];
|
||||
const snapshot = bridge && typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : null;
|
||||
const root =
|
||||
snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) && "root" in snapshot
|
||||
? snapshot.root
|
||||
: snapshot;
|
||||
const stack = root ? [root] : [];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) continue;
|
||||
if (node.data && node.data.text === text) return true;
|
||||
if (Array.isArray(node.children)) stack.push(...node.children);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ mindmapId, text },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
).catch((error) => {
|
||||
throw new Error(`reload_mismatch:${stage}:${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await fs.rm(OUTPUT_DIR, { recursive: true, force: true });
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const portReport = [checkPort(3000), checkPort(8000)];
|
||||
const screenshots = [];
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
|
||||
const page = await context.newPage();
|
||||
const createdIds = [];
|
||||
let result = {
|
||||
ok: false,
|
||||
task: TASK,
|
||||
baseUrl: BASE_URL,
|
||||
documentId: null,
|
||||
mindmapId: null,
|
||||
nodeCount: 0,
|
||||
edgeCount: 0,
|
||||
toolbarActions: null,
|
||||
sidebarActions: null,
|
||||
kernelRevision: null,
|
||||
adapterSource: null,
|
||||
viewScale: null,
|
||||
navigatorActions: null,
|
||||
contextMenuActions: null,
|
||||
screenshots,
|
||||
ports: portReport,
|
||||
};
|
||||
|
||||
try {
|
||||
await ensureAuthenticated(page, context.request);
|
||||
const doc = await createTempDocument(context.request, null);
|
||||
createdIds.push(doc.documentId);
|
||||
const title = `task166-mindmap-${Date.now().toString().slice(-6)}`;
|
||||
await renameDocument(context.request, doc.workspaceId, doc.documentId, title);
|
||||
result.documentId = doc.documentId;
|
||||
|
||||
await openDocument(page, doc.workspaceId, doc.documentId);
|
||||
await insertMindmapThroughSlash(page);
|
||||
const inserted = await assertMindmapReady(page, "insert");
|
||||
screenshots.push(await screenshot(page, "01-after-insert-floating-overlay"));
|
||||
screenshots.push(await screenshot(page, "toolbar-actions"));
|
||||
|
||||
const mindmapId = inserted.mindmapId;
|
||||
assert(mindmapId, "insert: mindmapId 缺失");
|
||||
result.mindmapId = mindmapId;
|
||||
const insertedProjection = await fetchAdapterProjection(page, doc.documentId, mindmapId);
|
||||
result.kernelRevision =
|
||||
typeof insertedProjection?.kernelRevision === "number" ? insertedProjection.kernelRevision : null;
|
||||
result.adapterSource =
|
||||
typeof insertedProjection?.source === "string"
|
||||
? insertedProjection.source
|
||||
: typeof insertedProjection?.compatPayload?.source === "string"
|
||||
? insertedProjection.compatPayload.source
|
||||
: null;
|
||||
|
||||
const editedText = `KMIND-${Date.now().toString().slice(-5)}`;
|
||||
await editRootNode(page, doc.documentId, mindmapId, editedText);
|
||||
const edited = await assertMindmapReady(page, "edit");
|
||||
screenshots.push(await screenshot(page, "02-after-edit"));
|
||||
|
||||
await verifyReadonlyToolbarState(page, "toolbar");
|
||||
const toolbarActions = await exerciseToolbarNodeActions(page, doc.documentId, mindmapId);
|
||||
assert(toolbarActions.insertChildOk, "command_failed:insertChild");
|
||||
assert(toolbarActions.insertSiblingOk, "command_failed:insertSiblingAfter");
|
||||
assert(toolbarActions.deleteOk, "command_failed:deleteNode");
|
||||
screenshots.push(await screenshot(page, "03-after-toolbar-actions"));
|
||||
|
||||
for (const [panelId, name] of [
|
||||
["nodeStyle", "sidebar-node-style"],
|
||||
["baseStyle", "sidebar-base-style"],
|
||||
["theme", "sidebar-theme"],
|
||||
["structure", "sidebar-structure"],
|
||||
["outline", "sidebar-outline"],
|
||||
]) {
|
||||
await page.getByTestId(`mindmap-schema-sidebar-tab-${panelId}`).click({ timeout: UI_TIMEOUT_MS });
|
||||
screenshots.push(await screenshot(page, name));
|
||||
}
|
||||
const sidebarActions = await exerciseSidebarActions(page, doc.documentId, mindmapId);
|
||||
assert(sidebarActions.themeOk, "command_failed:setTheme");
|
||||
assert(sidebarActions.layoutOk, "command_failed:setLayout");
|
||||
assert(sidebarActions.nodeStyleCompatOk, "command_failed:nodeStyleCompat");
|
||||
assert(sidebarActions.baseStyleCompatOk, "command_failed:baseStyleCompat");
|
||||
assert(sidebarActions.outlineDerivedOk, "ui_shell_missing:outline");
|
||||
screenshots.push(await screenshot(page, "sidebar-actions"));
|
||||
screenshots.push(await screenshot(page, "navigator-default"));
|
||||
const navigatorActions = await exerciseNavigatorActions(page, doc.documentId, mindmapId);
|
||||
assert(navigatorActions.centerRootOk, "view_command_failed:centerRoot");
|
||||
assert(navigatorActions.zoomOk, "view_command_failed:zoomIn");
|
||||
assert(navigatorActions.searchOk, "ui_shell_missing:search");
|
||||
assert(navigatorActions.minimapOk, "ui_shell_missing:minimap");
|
||||
screenshots.push(await screenshot(page, "navigator-zoomed"));
|
||||
screenshots.push(await screenshot(page, "navigator-minimap"));
|
||||
|
||||
const contextMenuActions = await exerciseContextMenuActions(page, mindmapId);
|
||||
assert(contextMenuActions.nodeMenuOk, "ui_shell_missing:node_context_menu");
|
||||
assert(contextMenuActions.canvasMenuOk, "ui_shell_missing:canvas_context_menu");
|
||||
assert(contextMenuActions.copyTextOk, "command_failed:copyNodeText");
|
||||
assert(contextMenuActions.insertChildOk, "command_failed:context_insertChild");
|
||||
screenshots.push(await screenshot(page, "context-menu-actions"));
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
const reloaded = await assertMindmapReady(page, "reload");
|
||||
await waitForMindmapText(page, mindmapId, editedText, "reload");
|
||||
const reloadStats = await readMindmapSnapshotStats(page, mindmapId);
|
||||
const expectedReloadNodeCount = contextMenuActions?.afterInsertNodeCount ?? toolbarActions.afterDeleteNodeCount;
|
||||
assert(
|
||||
reloadStats.nodeCount === expectedReloadNodeCount,
|
||||
`reload_mismatch:toolbar_node_count expected=${expectedReloadNodeCount} actual=${reloadStats.nodeCount}`,
|
||||
);
|
||||
const reloadProjection = await fetchAdapterProjection(page, doc.documentId, mindmapId);
|
||||
assert(reloadProjection?.theme === "classic4", `reload_mismatch:theme actual=${JSON.stringify(reloadProjection?.theme)}`);
|
||||
assert(reloadProjection?.layout === "mindMap", `reload_mismatch:layout actual=${JSON.stringify(reloadProjection?.layout)}`);
|
||||
assert(
|
||||
reloadProjection?.root?.data?.fillColor === "#dbeafe",
|
||||
`reload_mismatch:node_style actual=${JSON.stringify(reloadProjection?.root?.data)}`,
|
||||
);
|
||||
assert(
|
||||
reloadProjection?.compatPayload?.style?.map?.lineStyle === "curve",
|
||||
`reload_mismatch:base_style actual=${JSON.stringify(reloadProjection?.compatPayload?.style)}`,
|
||||
);
|
||||
screenshots.push(await screenshot(page, "04-after-reload"));
|
||||
|
||||
result = {
|
||||
...result,
|
||||
ok: true,
|
||||
nodeCount: reloaded.nodeCount,
|
||||
edgeCount: reloaded.edgeCount,
|
||||
toolbarActions,
|
||||
sidebarActions,
|
||||
navigatorActions,
|
||||
contextMenuActions,
|
||||
kernelRevision: result.kernelRevision,
|
||||
adapterSource: result.adapterSource,
|
||||
viewScale: navigatorActions.zoomScale,
|
||||
};
|
||||
await writeResult(result);
|
||||
process.stdout.write(`${JSON.stringify({ ...result, resultPath: RESULT_PATH }, null, 2)}\n`);
|
||||
} catch (error) {
|
||||
const metrics = await readMindmapMetrics(page).catch(() => null);
|
||||
const failureStage =
|
||||
metrics?.errorStage ||
|
||||
(error instanceof Error && /projection_load_failed|adapter_init_failed|runtime_render_failed|ui_shell_missing|command_failed|reload_mismatch/.test(error.message)
|
||||
? error.message
|
||||
: "runtime_render_failed");
|
||||
result = {
|
||||
...result,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
failureStage,
|
||||
nodeCount: metrics?.nodeCount ?? result.nodeCount,
|
||||
edgeCount: metrics?.edgeCount ?? result.edgeCount,
|
||||
};
|
||||
screenshots.push(await screenshot(page, "99-failure").catch(() => null));
|
||||
await writeResult(result);
|
||||
throw error;
|
||||
} finally {
|
||||
if (createdIds.length > 0) {
|
||||
await cleanupDocuments(context.request, createdIds).catch(() => undefined);
|
||||
}
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user