#!/usr/bin/env node "use strict"; const assert = require("node:assert"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { chromium } = require("playwright"); const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000); const OUT_DIR = path.join(process.cwd(), "tmp", "task455-local-folder-mindmap-clean-smoke"); const RESULT_PATH = path.join(OUT_DIR, "result.json"); const ACTOR_ID = "user_real"; const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium", "/usr/bin/chromium"] .find((candidate) => fs.existsSync(candidate)); function fileUrl(localPath) { return `file://${localPath}`; } function localMdDocumentId(relativePath) { return `local-md:${relativePath.replaceAll("/", "~2F")}`; } function documentUrl(root, relativePath) { const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`); url.searchParams.set("sourceKind", "local_folder"); url.searchParams.set("rootUri", fileUrl(root)); url.searchParams.set("treeView", "filetree"); return url.toString(); } function writeWorkspaceManifest(root) { const metadataDir = path.join(root, ".mnote"); fs.mkdirSync(metadataDir, { recursive: true }); fs.writeFileSync( path.join(metadataDir, "workspace.json"), `${JSON.stringify({ workspaceId: `local-ws:${ACTOR_ID}:task455`, ownerId: ACTOR_ID, createdAt: new Date().toISOString(), capabilities: ["local_files", "markdown_edit", "asset_upload"], }, null, 2)}\n`, "utf8", ); } async function screenshot(page, name) { const file = path.join(OUT_DIR, `${name}.png`); await page.screenshot({ path: file, fullPage: true }); return file; } async function openDocument(page, root, relativePath) { await page.goto(documentUrl(root, relativePath), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS, }); await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS, }); } async function insertMindmapThroughSlash(page) { const editor = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror[contenteditable="true"]').first(); 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 }); await page.locator('[data-testid="mnote-mindmap-editor-root"]').first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS, }); } async function assertMindmapStyleDrawerClosedByDefault(page) { await page.locator('[data-testid="mindmap-rust-shell"]').first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS, }); const state = await page.evaluate(() => { const shell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const sidebar = document.querySelector('[data-testid="mindmap-schema-sidebar"]'); const drawer = document.querySelector('[data-testid="mindmap-schema-sidebar-drawer"]'); return { panelOpen: shell instanceof HTMLElement ? shell.getAttribute("data-sidebar-panel-open") || "" : "", activePanel: shell instanceof HTMLElement ? shell.getAttribute("data-sidebar-active-panel") || "" : "", sidebarPresent: sidebar instanceof HTMLElement, sidebarPanelOpen: sidebar instanceof HTMLElement ? sidebar.getAttribute("data-panel-open") || "" : "", drawerVisible: drawer instanceof HTMLElement && drawer.getClientRects().length > 0, bodyText: document.body?.innerText || "", }; }); assert.equal(state.panelOpen, "false", `节点样式抽屉不应默认打开: ${JSON.stringify(state)}`); if (state.sidebarPanelOpen) { assert.equal(state.sidebarPanelOpen, "false", `sidebar panel 状态应与 shell 保持默认收起: ${JSON.stringify(state)}`); } assert.equal(state.drawerVisible, false, `节点样式抽屉不应默认渲染遮挡画布: ${JSON.stringify(state)}`); return state; } async function assertSlashMenuAnchorsAfterMindmap(page) { const editor = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror[contenteditable="true"]').first(); await editor.click({ timeout: UI_TIMEOUT_MS }); await page.keyboard.press(process.platform === "darwin" ? "Meta+End" : "Control+End"); await page.keyboard.type("/"); await page.getByTestId("mnote-leptos-tiptap-slash-menu").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => document.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]')?.getAttribute("data-mnote-slash-positioned") === "host", null, { timeout: UI_TIMEOUT_MS }, ); const state = await page.evaluate(() => { const menu = document.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]'); const mindmap = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const menuRect = menu?.getBoundingClientRect(); const mindmapRect = mindmap?.getBoundingClientRect(); const pointNode = menuRect ? document.elementFromPoint(menuRect.left + 24, menuRect.top + 24) : null; return { menuRect: menuRect ? { top: menuRect.top, bottom: menuRect.bottom, left: menuRect.left, right: menuRect.right } : null, mindmapRect: mindmapRect ? { top: mindmapRect.top, bottom: mindmapRect.bottom, left: mindmapRect.left, right: mindmapRect.right } : null, menuPosition: menu instanceof HTMLElement ? getComputedStyle(menu).position : "", menuZIndex: menu instanceof HTMLElement ? getComputedStyle(menu).zIndex : "", hostPositioned: menu instanceof HTMLElement ? menu.getAttribute("data-mnote-slash-positioned") || "" : "", hitInsideMenu: Boolean(pointNode && menu && menu.contains(pointNode)), viewportHeight: window.innerHeight, }; }); assert(state.menuRect, `slash 菜单应可见: ${JSON.stringify(state)}`); assert.equal(state.menuPosition, "fixed", `slash 菜单应由宿主定位到 viewport 层: ${JSON.stringify(state)}`); assert(Number(state.menuZIndex) >= 120, `slash 菜单层级应高于 mindmap/块工具: ${JSON.stringify(state)}`); assert(state.hitInsideMenu, `slash 菜单不应被思维导图或其它层遮挡: ${JSON.stringify(state)}`); assert(state.menuRect.top >= 0 && state.menuRect.bottom <= state.viewportHeight, `slash 菜单不应超出视口: ${JSON.stringify(state)}`); if (state.mindmapRect) { assert( state.menuRect.top > state.mindmapRect.top - 80, `mindmap 后输入 / 时菜单不应回退到编辑器左上固定旧位置: ${JSON.stringify(state)}`, ); } await page.keyboard.press("Escape"); return state; } async function resizeMindmapThroughCornerHandle(page) { await page.locator('[data-testid="mindmap-resize-handle-nw"]').first().waitFor({ state: "attached", timeout: UI_TIMEOUT_MS, }); const before = await page.evaluate(() => { const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]'); const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror'); const handles = Array.from(document.querySelectorAll('[data-mnote-mindmap-resize-handle]')) .map((handle) => handle.getAttribute("data-mnote-mindmap-resize-handle") || "") .sort(); const rect = placeholder?.getBoundingClientRect(); const editorRect = editor?.getBoundingClientRect(); return { handles, rect: rect ? { width: Math.round(rect.width), height: Math.round(rect.height) } : null, centerDelta: rect && editorRect ? Math.round((rect.left + rect.width / 2) - (editorRect.left + editorRect.width / 2)) : null, cssMaxWidth: root instanceof HTMLElement ? getComputedStyle(root).getPropertyValue("--mnote-mindmap-block-max-width").trim() : "", htmlCssMaxWidth: getComputedStyle(document.documentElement).getPropertyValue("--mnote-mindmap-block-max-width").trim(), }; }); assert.deepEqual(before.handles, ["ne", "nw", "se", "sw"], `mindmap 应渲染四角 resize handle: ${JSON.stringify(before)}`); assert(before.rect, `mindmap resize 前应能读取占位块尺寸: ${JSON.stringify(before)}`); assert(Math.abs(before.centerDelta) <= 2, `初始 mindmap 应以正文列中心对齐: ${JSON.stringify(before)}`); await page.waitForFunction(() => { const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; return Object.values(registry).some((bridge) => typeof bridge?.instance?.resize === "function"); }, null, { timeout: UI_TIMEOUT_MS }); await page.evaluate(() => { window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT = 0; const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; Object.values(registry).forEach((bridge) => { const instance = bridge?.instance; if (!instance || typeof instance.resize !== "function" || instance.resize.__mnoteResizeCounterPatched) return; const original = instance.resize.bind(instance); const patched = function patchedMindmapResize() { window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT = Number(window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT || 0) + 1; return original(); }; patched.__mnoteResizeCounterPatched = true; instance.resize = patched; }); }); const handleBox = await page.locator('[data-testid="mindmap-resize-handle-nw"]').first().boundingBox(); assert(handleBox, "左上角 resize handle 应有可交互位置"); await page.mouse.move(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2); await page.mouse.down(); await page.mouse.move(handleBox.x + 160, handleBox.y + 110, { steps: 8 }); await page.mouse.up(); await page.waitForFunction( ({ previousWidth, previousHeight }) => { const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); if (!(placeholder instanceof HTMLElement) || !(scene instanceof HTMLElement)) return false; const width = Number(placeholder.dataset.mnoteMindmapWidth || 0); const height = Number(placeholder.dataset.mnoteMindmapHeight || 0); return width >= 320 && height >= 240 && width < previousWidth && height < previousHeight && scene.dataset.mnoteMindmapHeight === String(height); }, { previousWidth: before.rect.width, previousHeight: before.rect.height }, { timeout: UI_TIMEOUT_MS }, ); const resized = await page.evaluate(() => { const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror'); const rect = placeholder?.getBoundingClientRect(); const editorRect = editor?.getBoundingClientRect(); return { width: placeholder instanceof HTMLElement ? Number(placeholder.dataset.mnoteMindmapWidth || 0) : 0, height: placeholder instanceof HTMLElement ? Number(placeholder.dataset.mnoteMindmapHeight || 0) : 0, sceneHeight: scene instanceof HTMLElement ? Number(scene.dataset.mnoteMindmapHeight || 0) : 0, rect: rect ? { width: Math.round(rect.width), height: Math.round(rect.height) } : null, centerDelta: rect && editorRect ? Math.round((rect.left + rect.width / 2) - (editorRect.left + editorRect.width / 2)) : null, cssMaxWidth: getComputedStyle(document.documentElement).getPropertyValue("--mnote-mindmap-block-max-width").trim(), resizeCallCount: Number(window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT || 0), }; }); assert(Math.abs(resized.centerDelta) <= 2, `resize 后 mindmap 仍应以正文列中心对齐: ${JSON.stringify(resized)}`); assert(resized.resizeCallCount <= 3, `拖动过程中不应连续触发 simple-mind-map resize 重绘: ${JSON.stringify(resized)}`); const afterGlobalWidthPreference = await page.evaluate(() => { const setMindmapWidthMax = (value) => { document.documentElement.style.setProperty("--mnote-mindmap-block-max-width", value); document.querySelector('.document-shell')?.style.setProperty("--mnote-mindmap-block-max-width", value); document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')?.style.setProperty("--mnote-mindmap-block-max-width", value); window.dispatchEvent(new CustomEvent("mnote:page-width-preference-changed", { detail: { type: "mindmap" } })); }; setMindmapWidthMax("720px"); return true; }); assert.equal(afterGlobalWidthPreference, true); await page.waitForFunction(() => { const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]'); const rect = placeholder?.getBoundingClientRect(); return placeholder instanceof HTMLElement && !placeholder.dataset.mnoteMindmapWidth && rect && Math.round(rect.width) >= 900 && Math.round(rect.width) <= 920; }, null, { timeout: UI_TIMEOUT_MS }); const globalWidthState = await page.evaluate(() => { const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]'); const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror'); const rect = placeholder?.getBoundingClientRect(); const editorRect = editor?.getBoundingClientRect(); return { widthAttr: placeholder instanceof HTMLElement ? placeholder.dataset.mnoteMindmapWidth || "" : "", heightAttr: placeholder instanceof HTMLElement ? placeholder.dataset.mnoteMindmapHeight || "" : "", rect: rect ? { width: Math.round(rect.width), height: Math.round(rect.height) } : null, centerDelta: rect && editorRect ? Math.round((rect.left + rect.width / 2) - (editorRect.left + editorRect.width / 2)) : null, cssMaxWidth: getComputedStyle(document.documentElement).getPropertyValue("--mnote-mindmap-block-max-width").trim(), }; }); assert.equal(globalWidthState.widthAttr, "", `全局 Mindmap 宽度设置后应清除本块手动宽度: ${JSON.stringify(globalWidthState)}`); assert(Math.abs(globalWidthState.centerDelta) <= 2, `全局 Mindmap 宽度设置后仍应居中: ${JSON.stringify(globalWidthState)}`); return { ...resized, globalWidthState }; } async function readState(page) { return page.evaluate(() => { const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]'); const mindmapRoot = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const rustShell = document.querySelector('[data-testid="mindmap-rust-shell"]'); const fileTreeRows = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')) .map((row) => { const element = row; return { rowId: element.getAttribute("data-row-id") || "", rowKind: element.getAttribute("data-row-kind") || "", title: element.textContent || "", assetId: element.getAttribute("data-asset-id") || "", objectKind: element.getAttribute("data-object-kind") || "", documentId: element.getAttribute("data-document-id") || element.getAttribute("data-doc-id") || "", expanded: element.getAttribute("aria-expanded") || "", }; }); const mindmapRows = fileTreeRows.filter((row) => row.objectKind === "mindmap" || row.assetId.endsWith(".json") || row.title.includes("思维导图")); return { url: window.location.href, editorStatus: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-runtime-editor-status") || "" : "", mindmapId: mindmapRoot instanceof HTMLElement ? mindmapRoot.dataset.mnoteMindmapId || "" : "", shellMindmapId: rustShell instanceof HTMLElement ? rustShell.getAttribute("data-mnote-mindmap-id") || "" : "", bodyText: document.body?.innerText || "", fileTreeRows, mindmapRows, consoleMarker: document.documentElement.getAttribute("data-mnote-last-mindmap-asset-id") || "", }; }); } async function waitForStableEditorSave(page, networkRecords, label) { const beforeCount = networkRecords.length; let lastState = null; const deadline = Date.now() + UI_TIMEOUT_MS; while (Date.now() < deadline) { lastState = await readState(page); const pageBodyWrites = networkRecords .slice(beforeCount) .filter((record) => record.url.includes("/api/page-body/write")); const lastWrite = pageBodyWrites[pageBodyWrites.length - 1] || null; if (lastWrite && lastWrite.status >= 200 && lastWrite.status < 300 && lastState.editorStatus === "saved") { return { ok: true, label, lastState, pageBodyWrites }; } if (lastState.editorStatus === "error" || lastState.editorStatus === "external-change-conflict") { break; } await page.waitForTimeout(250); } return { ok: false, label, lastState, pageBodyWrites: networkRecords.slice(beforeCount).filter((record) => record.url.includes("/api/page-body/write")), }; } async function waitForMindmapId(page) { await page.waitForFunction( () => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const mindmapId = root instanceof HTMLElement ? String(root.dataset.mnoteMindmapId || "") : ""; return /^思维导图\d{6}\.json$/.test(mindmapId) || /^mindmap[-_][^/\\]+(?:\.json)?$/.test(mindmapId); }, null, { timeout: UI_TIMEOUT_MS }, ); const state = await readState(page); return state.mindmapId; } function localMindmapFileName(mindmapId) { const value = String(mindmapId || "").trim(); return value.toLowerCase().endsWith(".json") ? value : `${value}.json`; } function rowMatchesMindmap(row, mindmapId) { const assetId = String(row && row.assetId || ""); const title = String(row && row.title || ""); return assetId === mindmapId || assetId.endsWith(`/${mindmapId}`) || assetId.endsWith(`:${mindmapId}`) || title.includes(mindmapId); } async function sampleFileTree(page, mindmapId, durationMs = 5_000) { const samples = []; const deadline = Date.now() + durationMs; while (Date.now() < deadline) { const state = await readState(page); samples.push({ at: Date.now(), mindmapId: state.mindmapId, mindmapRows: state.mindmapRows.map((row) => ({ rowId: row.rowId, assetId: row.assetId, title: row.title, objectKind: row.objectKind, })), }); assert.equal( state.mindmapRows.filter((row) => rowMatchesMindmap(row, mindmapId)).length, 1, `本轮 mindmap 文件树行应稳定存在且仅一行: ${JSON.stringify(state.mindmapRows)}`, ); assert( !state.mindmapRows.some((row) => row.assetId === "mindmap" || row.title.includes("思维导图.json")), `不应出现退化的 mindmap 资源行: ${JSON.stringify(state.mindmapRows)}`, ); await page.waitForTimeout(250); } return samples; } async function postMindmapCommand(page, documentId, mindmapId) { return page.evaluate(async ({ documentId, mindmapId }) => { const response = await fetch(`/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ commandName: "mindmap.command.apply", commands: [{ type: "updateText", mindmapId, nodeId: "root", text: "KMIND 本轮验证", }], projectionRevision: 1, }), }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error(`mindmap_command_failed_${response.status}:${JSON.stringify(payload)}`); } return payload; }, { documentId, mindmapId }); } async function main() { fs.mkdirSync(OUT_DIR, { recursive: true }); const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task455-mindmap-")); const pageDir = path.join(root, "CleanPage"); fs.mkdirSync(pageDir, { recursive: true }); writeWorkspaceManifest(root); const relativePath = "CleanPage/CleanPage.md"; const markdownPath = path.join(root, relativePath); fs.writeFileSync(markdownPath, "# CleanPage\n\n本轮 local mindmap clean smoke。\n", "utf8"); const documentId = localMdDocumentId(relativePath); const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1", executablePath: CHROMIUM_EXECUTABLE_PATH, }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, extraHTTPHeaders: { "x-mnote-actor-id": ACTOR_ID, "x-mnote-actor-type": "user", "cache-control": "no-store", }, }); const page = await context.newPage(); const consoleMessages = []; const networkRecords = []; page.on("console", (message) => { if (["error", "warning"].includes(message.type()) || /mindmap|local-folder|502|403|500/i.test(message.text())) { consoleMessages.push({ type: message.type(), text: message.text() }); } }); page.on("response", (response) => { const url = response.url(); if (/\/api\/mindmap\/|\/api\/page-body\/write|\/api\/tree\/projections\/file|\/api\/local-folder/.test(url)) { networkRecords.push({ status: response.status(), method: response.request().method(), url }); } }); const result = { ok: false, baseUrl: BASE_URL, root, relativePath, documentId, mindmapId: "", screenshots: [], consoleMessages, networkRecords, diskBefore: [], diskAfterInsert: [], diskAfterRefresh: [], samples: [], markdown: "", saveAfterInsert: null, saveAfterCommand: null, markdownMissingMindmapReferenceAfterInsert: false, refreshSkippedBecauseMarkdownNotSaved: false, commandResponseSummary: null, resizeAfterInsert: null, }; try { await openDocument(page, root, relativePath); result.screenshots.push(await screenshot(page, "01-open-clean-page")); await insertMindmapThroughSlash(page); result.mindmapId = await waitForMindmapId(page); const mindmapFileName = localMindmapFileName(result.mindmapId); result.screenshots.push(await screenshot(page, "02-after-insert-mindmap")); result.defaultStyleDrawer = await assertMindmapStyleDrawerClosedByDefault(page); result.resizeAfterInsert = await resizeMindmapThroughCornerHandle(page); await page.waitForFunction( ({ expected }) => { const tree = document.getElementById("sidebar-file-tree-root"); return (tree?.textContent || "").includes(expected); }, { expected: mindmapFileName }, { timeout: UI_TIMEOUT_MS }, ); result.saveAfterInsert = await waitForStableEditorSave(page, networkRecords, "after-insert"); result.slashMenuAfterMindmap = await assertSlashMenuAnchorsAfterMindmap(page); result.diskAfterInsert = fs.readdirSync(pageDir).sort(); assert(result.diskAfterInsert.includes("CleanPage.md"), `页面 Markdown 应存在: ${result.diskAfterInsert.join(",")}`); assert(result.diskAfterInsert.includes(mindmapFileName), `mindmap 应直接出现在页面文件夹下: ${result.diskAfterInsert.join(",")}`); assert(!fs.existsSync(path.join(root, mindmapFileName)), "root 同级不应残留 mindmap 文件"); assert(!fs.existsSync(path.join(pageDir, "assets", mindmapFileName)), "assets 下不应残留 mindmap 文件"); result.markdown = fs.readFileSync(markdownPath, "utf8"); result.markdownMissingMindmapReferenceAfterInsert = !result.markdown.includes(`](${mindmapFileName})`); result.samples = await sampleFileTree(page, result.mindmapId, 4_000); result.commandResponseSummary = await postMindmapCommand(page, documentId, result.mindmapId); result.saveAfterCommand = await waitForStableEditorSave(page, networkRecords, "after-command"); result.samples = result.samples.concat(await sampleFileTree(page, result.mindmapId, 4_000)); result.screenshots.push(await screenshot(page, "03-after-command-apply")); if (result.markdownMissingMindmapReferenceAfterInsert) { result.refreshSkippedBecauseMarkdownNotSaved = true; } else { await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await page.locator('[data-testid="mnote-mindmap-editor-root"]').first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS, }); const afterRefresh = await readState(page); assert.equal(localMindmapFileName(afterRefresh.mindmapId), mindmapFileName, `刷新后 mindmapId 应保持不变: ${JSON.stringify(afterRefresh)}`); assert( afterRefresh.mindmapRows.filter((row) => rowMatchesMindmap(row, result.mindmapId)).length === 1, `刷新后文件树应只有本轮 mindmap 一行: ${JSON.stringify(afterRefresh.mindmapRows)}`, ); assert( !afterRefresh.mindmapRows.some((row) => row.assetId === "mindmap" || row.title.includes("思维导图.json")), `刷新后不应出现退化 mindmap 行: ${JSON.stringify(afterRefresh.mindmapRows)}`, ); result.diskAfterRefresh = fs.readdirSync(pageDir).sort(); result.screenshots.push(await screenshot(page, "04-after-refresh")); } const blockingNetworkErrors = networkRecords.filter((record) => { if (record.status < 400) return false; return !record.url.includes("/api/local-folder/events"); }); assert( blockingNetworkErrors.length === 0, `mindmap smoke 不应出现阻断性 4xx/5xx API 响应: ${JSON.stringify(blockingNetworkErrors)}`, ); result.ok = true; } catch (error) { result.error = error && error.stack ? error.stack : String(error); try { result.screenshots.push(await screenshot(page, "99-failure")); } catch (_) { // 失败截图不可用时只保留错误文本。 } throw error; } finally { fs.writeFileSync(RESULT_PATH, `${JSON.stringify({ ...result, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8"); await browser.close().catch(() => {}); } console.log(`task455 local folder mindmap clean smoke passed: ${RESULT_PATH}`); } main().catch((error) => { console.error(error && error.stack ? error.stack : error); process.exit(1); });