#!/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, openFilesystemView, renameDocument, requestJson, } = require("./tree-shell-smoke-helpers"); const TASK = "task444-filetree-create-mindmap-title-cache-smoke"; const OUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUT_DIR, "result.json"); async function writeResult(result) { await fs.mkdir(OUT_DIR, { recursive: true }); await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...result, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8"); } async function createMindmap(request, workspaceId, documentId, mindmapId, title) { return await requestJson(request, `/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, { method: "POST", data: { commandName: "mindmaps.put", workspaceId, createOnly: true, data: { root: { data: { text: title }, children: [] } }, }, }); } async function readFileTreeRow(page, documentId) { return await page.evaluate((docId) => { const row = document.querySelector(`#sidebar-file-tree-root .tree-row[data-row-id="doc:${CSS.escape(docId)}"]`); if (!(row instanceof HTMLElement)) return null; const title = row.querySelector(":scope > .tree-link > .tree-link-title"); const rect = row.getBoundingClientRect(); return { rowId: row.getAttribute("data-row-id") || "", selected: row.getAttribute("data-selected") || "", focused: row.getAttribute("data-focused") || "", title: title instanceof HTMLElement ? (title.textContent || "").trim() : "", visible: rect.width > 0 && rect.height > 0, }; }, documentId); } async function createPageFromFileTree(page) { await openFilesystemView(page); const beforeIds = await page.evaluate(() => Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-row-id^="doc:"]')).map((row) => row.getAttribute("data-row-id") || "", ), ); await page.waitForSelector('#sidebar-file-tree-root [data-testid="filetree-create"]', { state: "attached", timeout: UI_TIMEOUT_MS }); await page.evaluate(() => { const button = document.querySelector('#sidebar-file-tree-root [data-testid="filetree-create"]'); if (!(button instanceof HTMLElement)) throw new Error("filetree_create_button_missing"); button.click(); }); await page.waitForURL((url) => url.pathname.includes("/documents/"), { timeout: UI_TIMEOUT_MS }).catch(() => null); const after = await page.waitForFunction( (knownIds) => { const rows = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-row-id^="doc:"]')); const created = rows.find((row) => { const rowId = row.getAttribute("data-row-id") || ""; return rowId && !knownIds.includes(rowId); }); if (!(created instanceof HTMLElement)) return null; const title = created.querySelector(":scope > .tree-link > .tree-link-title"); return { documentId: (created.getAttribute("data-document-id") || created.getAttribute("data-doc-id") || "").trim(), rowId: created.getAttribute("data-row-id") || "", title: title instanceof HTMLElement ? (title.textContent || "").trim() : "", }; }, beforeIds, { timeout: UI_TIMEOUT_MS }, ); return await after.jsonValue(); } (async () => { await fs.mkdir(OUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await context.newPage(); const consoleMessages = []; const navEvents = []; page.on("console", (message) => { const text = message.text(); if (message.type() === "error" || /activeRowId|ReferenceError|filetree|tree live/i.test(text)) { consoleMessages.push({ type: message.type(), text: text.slice(0, 2000) }); } }); page.on("pageerror", (error) => { consoleMessages.push({ type: "pageerror", text: error instanceof Error ? error.stack || error.message : String(error) }); }); page.on("framenavigated", (frame) => { if (frame === page.mainFrame()) navEvents.push({ at: Date.now(), url: frame.url() }); }); const createdDocuments = []; const result = { baseUrl: BASE_URL, fixture: {}, fileTreeCreated: null, afterLive: null, afterMindmapClick: null, afterPageClick: null, consoleMessages, navEvents, screenshots: [], }; try { await ensureAuthenticated(page, context.request); const root = await createTempDocument(context.request, null); createdDocuments.push(root.documentId); const stamp = Date.now().toString().slice(-8); await renameDocument(context.request, root.workspaceId, root.documentId, `TEST-444-root-${stamp}`); const mindmapId = `mindmap_444_${stamp}`; await createMindmap(context.request, root.workspaceId, root.documentId, mindmapId, `TEST-444-mind-${stamp}`); result.fixture = { ...root, mindmapId }; await openDocument(page, root.workspaceId, root.documentId); await openFilesystemView(page); await page.waitForSelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${mindmapId}"]`, { timeout: UI_TIMEOUT_MS }); result.fileTreeCreated = await createPageFromFileTree(page); assert(result.fileTreeCreated && result.fileTreeCreated.documentId, "文件树新建后未找到新页面 row"); createdDocuments.push(result.fileTreeCreated.documentId); assert( result.fileTreeCreated.title.endsWith(".md"), `文件树新建后的本地 row 应显示 .md: ${JSON.stringify(result.fileTreeCreated)}`, ); await page.waitForTimeout(1500); result.afterLive = await readFileTreeRow(page, result.fileTreeCreated.documentId); assert(result.afterLive && result.afterLive.title.endsWith(".md"), `live 后文件树标题不应丢 .md: ${JSON.stringify(result.afterLive)}`); await page.click(`#sidebar-file-tree-root .tree-row[data-asset-id="${mindmapId}"] .tree-link`, { timeout: UI_TIMEOUT_MS }); await page.waitForURL((url) => url.pathname.includes(`/mindmap/${encodeURIComponent(root.documentId)}/${encodeURIComponent(mindmapId)}`), { timeout: UI_TIMEOUT_MS, }); await page.waitForSelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${mindmapId}"][data-selected="true"]`, { timeout: UI_TIMEOUT_MS, }); result.afterMindmapClick = await readFileTreeRow(page, result.fileTreeCreated.documentId); await page.click(`#sidebar-file-tree-root .tree-row[data-row-id="doc:${result.fileTreeCreated.documentId}"] .tree-link`, { timeout: UI_TIMEOUT_MS, }); await page.waitForURL((url) => url.pathname.includes(`/documents/${encodeURIComponent(result.fileTreeCreated.documentId)}`), { timeout: UI_TIMEOUT_MS, }); await page.waitForSelector(`#sidebar-file-tree-root .tree-row[data-row-id="doc:${result.fileTreeCreated.documentId}"][data-selected="true"]`, { timeout: UI_TIMEOUT_MS, }); await page.waitForTimeout(500); result.afterPageClick = await readFileTreeRow(page, result.fileTreeCreated.documentId); assert( result.afterPageClick && result.afterPageClick.title.endsWith(".md"), `mindmap 后再点页面时文件树标题不应丢 .md: ${JSON.stringify(result.afterPageClick)}`, ); assert( !consoleMessages.some((message) => /activeRowId|ReferenceError/i.test(message.text)), `不应出现 activeRowId/ReferenceError: ${JSON.stringify(consoleMessages)}`, ); const screenshotPath = path.join(OUT_DIR, "after-page-click.png"); await page.screenshot({ path: screenshotPath, fullPage: true }); result.screenshots.push(screenshotPath); await writeResult({ ...result, ok: true, finalUrl: page.url() }); console.log(`ok ${TASK} ${RESULT_PATH}`); } catch (error) { await writeResult({ ...result, ok: false, currentUrl: page.url(), error: error instanceof Error ? error.stack || error.message : String(error), }); throw error; } finally { if (createdDocuments.length) await cleanupDocuments(context.request, createdDocuments).catch(() => null); await browser.close().catch(() => null); } })().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : error); process.exit(1); });