#!/usr/bin/env node "use strict"; const assert = require("node:assert/strict"); const fs = require("node:fs"); const fsp = require("node:fs/promises"); 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 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)); const TASK = "task443-filetree-mindmap-click-active-row-smoke"; const OUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUT_DIR, "result.json"); const ACTOR_ID = "user_real"; 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) { fs.mkdirSync(path.join(root, ".mnote"), { recursive: true }); fs.writeFileSync( path.join(root, ".mnote", "workspace.json"), `${JSON.stringify({ workspaceId: `local-ws:${ACTOR_ID}:task443`, ownerId: ACTOR_ID, createdAt: new Date().toISOString(), capabilities: ["local_files", "tree_commands", "markdown_edit", "asset_upload"], }, null, 2)}\n`, "utf8", ); } function writeMindmapFixture(root, stamp) { const pageDir = path.join(root, "Task443"); fs.mkdirSync(pageDir, { recursive: true }); fs.writeFileSync( path.join(pageDir, "Task443.md"), [ "---", `title: TEST-443-mindmap-active-${stamp}`, "---", "", "# TEST 443", "", `[TEST-443-mind-${stamp}](map-${stamp}.mindmap.json)`, "", ].join("\n"), "utf8", ); fs.writeFileSync( path.join(pageDir, `map-${stamp}.mindmap.json`), `${JSON.stringify({ data: { uid: "root", text: `TEST-443-mind-${stamp}` }, children: [] }, null, 2)}\n`, "utf8", ); return { relativePath: "Task443/Task443.md", documentId: localMdDocumentId("Task443/Task443.md"), mindmapFileName: `map-${stamp}.mindmap.json`, }; } async function writeResult(result) { await fsp.mkdir(OUT_DIR, { recursive: true }); await fsp.writeFile(RESULT_PATH, `${JSON.stringify({ ...result, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8"); } async function waitForMindmapRow(page, mindmapFileName) { await page.waitForFunction((fileName) => { return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')) .some((row) => (row.getAttribute("data-asset-id") || "").includes(fileName)); }, mindmapFileName, { timeout: UI_TIMEOUT_MS }); } async function clickMindmapRow(page, mindmapFileName) { await page.evaluate((fileName) => { const row = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')) .find((candidate) => (candidate.getAttribute("data-asset-id") || "").includes(fileName)); const link = row?.querySelector(".tree-link"); if (!(link instanceof HTMLElement)) { throw new Error(`找不到 mindmap 文件行: ${fileName}`); } link.click(); }, mindmapFileName); } async function readSelectedFileTreeRows(page) { return await page.evaluate(() => Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-selected="true"]')).map((row) => ({ rowId: row.getAttribute("data-row-id") || "", rowKind: row.getAttribute("data-row-kind") || "", docId: row.getAttribute("data-document-id") || row.getAttribute("data-doc-id") || "", ownerDocumentId: row.getAttribute("data-owner-document-id") || "", assetId: row.getAttribute("data-asset-id") || "", objectIdentity: row.getAttribute("data-object-identity") || "", title: (row.textContent || "").trim().slice(0, 160), })), ); } (async () => { await fsp.mkdir(OUT_DIR, { recursive: true }); const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task443-local-mindmap-")); const stamp = Date.now().toString().slice(-8); writeWorkspaceManifest(root); const fixture = writeMindmapFixture(root, stamp); const browser = await chromium.launch({ headless: true, ...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}), }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, extraHTTPHeaders: { "x-mnote-actor-id": ACTOR_ID, "x-mnote-actor-type": "user", }, }); const page = await context.newPage(); const navEvents = []; const network = []; page.on("framenavigated", (frame) => { if (frame === page.mainFrame()) navEvents.push({ at: Date.now(), url: frame.url() }); }); page.on("request", (request) => { const url = request.url(); if (url.includes("/mindmap/") || url.includes("/documents/") || url.includes("/api/tree/events")) { network.push({ type: "request", method: request.method(), url }); } }); const result = { baseUrl: BASE_URL, fixture: { root, ...fixture }, beforeSelectedRows: [], afterSelectedRows: [], navEvents, network, screenshots: [], }; try { await page.goto(documentUrl(root, fixture.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 waitForMindmapRow(page, fixture.mindmapFileName); result.beforeSelectedRows = await readSelectedFileTreeRows(page); await clickMindmapRow(page, fixture.mindmapFileName); await page.waitForFunction( ({ documentId, fileName }) => { const url = new URL(window.location.href); if (!url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`)) return false; const rt = decodeURIComponent(url.searchParams.get("resourceTab") || ""); return rt.includes("resource:mindmap:") && rt.includes(documentId) && rt.includes(fileName); }, { documentId: fixture.documentId, fileName: fixture.mindmapFileName }, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction((fileName) => { return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-selected="true"]')) .some((row) => (row.getAttribute("data-asset-id") || "").includes(fileName)); }, fixture.mindmapFileName, { timeout: UI_TIMEOUT_MS }); result.afterUrl = page.url(); result.afterSelectedRows = await readSelectedFileTreeRows(page); const screenshotPath = path.join(OUT_DIR, "after-mindmap-click.png"); await page.screenshot({ path: screenshotPath, fullPage: true }); result.screenshots.push(screenshotPath); assert( result.afterSelectedRows.some((row) => row.assetId.includes(fixture.mindmapFileName)), `点击 mindmap 文件行后应保持 asset row 选中: ${JSON.stringify(result.afterSelectedRows)}`, ); assert( !result.afterSelectedRows.some((row) => row.rowId === `doc:${fixture.documentId}`), `点击 mindmap 文件行后不应闪回父页面行选中: ${JSON.stringify(result.afterSelectedRows)}`, ); await writeResult({ ...result, ok: true }); console.log(`ok ${TASK} ${RESULT_PATH}`); } catch (error) { await writeResult({ ...result, ok: false, error: error instanceof Error ? error.stack || error.message : String(error), currentUrl: page.url(), }); throw error; } finally { await browser.close().catch(() => null); fs.rmSync(root, { recursive: true, force: true }); } })().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : error); process.exit(1); });