#!/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 = "task168-mindmap-put-validator-smoke"; const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); const BAD_TEXT_PATTERN = /ArgumentValidationError|domainEventHint|domainEventPlan|streamDeltaHint|command_failed|502 Bad Gateway/i; 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; } function attachMindmapNetworkCapture(page, label, records) { page.on("response", async (response) => { const url = response.url(); if (!url.includes("/api/mindmap/") && !url.includes("/mindmap/") && !url.includes("/api/documents/save")) { return; } const request = response.request(); const method = request.method(); const status = response.status(); let responseText = null; if (method !== "GET" || status >= 400) { responseText = await response.text().catch((error) => `<>`); } records.push({ type: "response", label, method, url, status, statusText: response.statusText(), requestBody: request.postData() || null, responseText: responseText ? responseText.slice(0, 3000) : null, }); }); page.on("requestfailed", (request) => { const url = request.url(); if (url.includes("/api/mindmap/") || url.includes("/mindmap/") || url.includes("/api/documents/save")) { records.push({ type: "requestfailed", label, method: request.method(), url, failure: request.failure()?.errorText || null, requestBody: request.postData() || null, }); } }); page.on("console", (message) => { if (message.type() === "error") { const text = message.text(); if (/mindmap|ArgumentValidationError|domainEvent|502|command_failed/i.test(text)) { records.push({ type: "console", label, level: message.type(), text: text.slice(0, 2000), }); } } }); } 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 readMindmapState(page) { return page.evaluate(() => { const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]'); const error = document.querySelector('[data-testid="leptos-mindmap-error"]'); return { mindmapId: root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null, runtimeReady: runtime instanceof HTMLElement ? runtime.dataset.runtimeReady === "true" : false, commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || null : null, lastSchemaAction: scene instanceof HTMLElement ? scene.dataset.lastSchemaAction || null : null, commandRuntimeError: scene instanceof HTMLElement ? scene.dataset.commandRuntimeError || null : null, commandRuntimeMessage: scene instanceof HTMLElement ? scene.dataset.commandRuntimeMessage || null : null, errorStage: error instanceof HTMLElement ? error.dataset.stage || null : null, errorText: error instanceof HTMLElement ? (error.textContent || "").slice(0, 3000) : null, bodyText: (document.body?.innerText || "").slice(0, 5000), }; }); } async function assertNoValidatorLeak(page, records, stage) { await page.waitForTimeout(500); const state = await readMindmapState(page); const badRecord = records.find((record) => { if (record.type === "response" && record.status >= 500) return true; return BAD_TEXT_PATTERN.test(`${record.responseText || ""}\n${record.text || ""}\n${record.failure || ""}`); }); if (badRecord || BAD_TEXT_PATTERN.test(`${state.errorText || ""}\n${state.bodyText || ""}`)) { throw new Error( `${stage}:mindmap_validator_or_502_leak:${JSON.stringify( { state, badRecord, recentMindmapNetwork: records.slice(-12), }, null, 2, )}`, ); } return state; } async function waitForMindmapReady(page, stage) { 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__ || {}; return ( root instanceof HTMLElement && runtime instanceof HTMLElement && runtime.dataset.runtimeReady === "true" && Boolean(mindmapId) && Boolean(registry[mindmapId]) ); }, null, { timeout: UI_TIMEOUT_MS }, ) .catch((error) => { throw new Error(`${stage}:mindmap_not_ready:${error.message}`); }); } async function waitForDocumentContentToIncludeMindmap(requestContext, documentId, workspaceId, mindmapId) { let lastText = ""; for (let index = 0; index < 45; index += 1) { const response = await requestContext.fetch( `${BASE_URL}/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`, { method: "GET", timeout: 10_000 }, ); lastText = await response.text(); if (response.ok() && lastText.includes(mindmapId)) { try { return JSON.parse(lastText); } catch { return { raw: lastText.slice(0, 3000) }; } } await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error( `document_content_missing_mindmap:${JSON.stringify({ documentId, workspaceId, mindmapId, lastText: lastText.slice(0, 5000), })}`, ); } async function fetchMindmapProjection(requestContext, documentId, mindmapId) { const response = await requestContext.fetch( `${BASE_URL}/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`, { method: "GET", timeout: 10_000 }, ); const text = await response.text(); assert(response.ok(), `mindmap_projection_fetch_failed:${response.status()}:${text.slice(0, 2000)}`); const payload = JSON.parse(text); return payload.result ?? payload; } function collectMindmapTexts(root) { const texts = []; const visit = (node) => { if (!node || typeof node !== "object") return; const data = node.data && typeof node.data === "object" ? node.data : {}; if (typeof data.text === "string") texts.push(data.text); if (Array.isArray(node.children)) node.children.forEach(visit); }; visit(root); return texts; } async function waitForProjectionText(requestContext, documentId, mindmapId, expectedText) { let lastProjection = null; for (let index = 0; index < 45; index += 1) { lastProjection = await fetchMindmapProjection(requestContext, documentId, mindmapId); const texts = collectMindmapTexts(lastProjection.root); if (texts.includes(expectedText)) { return { projection: lastProjection, texts }; } await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error( `mindmap_projection_missing_edited_text:${JSON.stringify({ expectedText, texts: collectMindmapTexts(lastProjection?.root), projection: lastProjection, }).slice(0, 5000)}`, ); } async function editTopicTextThroughRuntime(page, mindmapId, text) { const result = await page.evaluate( ({ mindmapId, text }) => { const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); if (scene instanceof HTMLElement) { scene.dataset.lastDataChangeRefreshTriggered = ""; scene.dataset.lastDataChangeDiffCommandCount = ""; scene.dataset.commandStatus = "smoke-waiting-topic-edit"; } const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = registry[mindmapId]; const instance = bridge?.instance; const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null; if (!topicNode || typeof topicNode !== "object") { return { ok: false, reason: "topic_node_missing" }; } if (typeof topicNode.setData === "function") { topicNode.setData({ text }); } else if (topicNode.data && typeof topicNode.data === "object") { topicNode.data.text = text; } else { return { ok: false, reason: "topic_node_not_mutable" }; } const snapshot = typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : instance?.getData?.(true); return { ok: true, snapshotText: snapshot?.children?.[0]?.data?.text ?? null, commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || null : null, diffCommandCount: scene instanceof HTMLElement ? scene.dataset.lastDataChangeDiffCommandCount || null : null, }; }, { mindmapId, text }, ); assert(result.ok, `topic_runtime_edit_failed:${JSON.stringify(result)}`); await page.waitForFunction( ({ text }) => { const bodyText = document.body?.innerText || ""; const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); return ( bodyText.includes(text) || (scene instanceof HTMLElement && scene.dataset.commandStatus !== "smoke-waiting-topic-edit") ); }, { text }, { timeout: UI_TIMEOUT_MS }, ); return result; } async function clickInsertChild(page) { const button = page.getByTestId("mindmap-schema-toolbar-action-insertChild"); await button.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); const disabled = await button.evaluate((node) => node instanceof HTMLButtonElement && node.disabled); if (disabled) { await page.locator('[data-testid="simple-mind-map-runtime"] .smm-node').first().click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction(() => { const button = document.querySelector('[data-testid="mindmap-schema-toolbar-action-insertChild"]'); return button instanceof HTMLButtonElement && !button.disabled; }, null, { timeout: UI_TIMEOUT_MS }); } 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 button.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 }); } async function main() { await fs.mkdir(OUTPUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const contextA = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" }); const pageA = await contextA.newPage(); const records = []; attachMindmapNetworkCapture(pageA, "browser-a", records); const screenshots = []; const createdIds = []; const result = { ok: false, task: TASK, baseUrl: BASE_URL, documentId: null, workspaceId: null, mindmapId: null, network: records, screenshots, }; let contextB = null; let pageB = null; try { await ensureAuthenticated(pageA, contextA.request); const doc = await createTempDocument(contextA.request, null); createdIds.push(doc.documentId); result.documentId = doc.documentId; result.workspaceId = doc.workspaceId; await renameDocument(contextA.request, doc.workspaceId, doc.documentId, `task168-mindmap-${Date.now().toString().slice(-6)}`); await openDocument(pageA, doc.workspaceId, doc.documentId); await insertMindmapThroughSlash(pageA); await waitForMindmapReady(pageA, "browser-a-initial"); const initialState = await assertNoValidatorLeak(pageA, records, "browser-a-initial"); result.mindmapId = initialState.mindmapId; await clickInsertChild(pageA); const afterCommand = await assertNoValidatorLeak(pageA, records, "browser-a-insert-child"); assert(afterCommand.commandStatus === "success", `insert_child_not_success:${JSON.stringify(afterCommand)}`); screenshots.push(await screenshot(pageA, "01-after-insert-child")); result.documentContentAfterInsert = await waitForDocumentContentToIncludeMindmap( contextA.request, doc.documentId, doc.workspaceId, result.mindmapId, ); const editedTopicText = `二级节点-SMOKE-${Date.now().toString().slice(-6)}`; result.topicEditAttempt = await editTopicTextThroughRuntime(pageA, result.mindmapId, editedTopicText); const afterTopicEdit = await assertNoValidatorLeak(pageA, records, "browser-a-topic-edit"); result.afterTopicEdit = afterTopicEdit; result.topicProjectionAfterEdit = await waitForProjectionText( contextA.request, doc.documentId, result.mindmapId, editedTopicText, ); screenshots.push(await screenshot(pageA, "02-after-topic-edit")); contextB = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" }); pageB = await contextB.newPage(); attachMindmapNetworkCapture(pageB, "browser-b", records); await ensureAuthenticated(pageB, contextB.request); await openDocument(pageB, doc.workspaceId, doc.documentId); await waitForMindmapReady(pageB, "browser-b-reopen"); const secondState = await assertNoValidatorLeak(pageB, records, "browser-b-reopen"); assert( secondState.bodyText.includes(editedTopicText), `second_browser_missing_edited_topic_text:${JSON.stringify({ expected: editedTopicText, secondState, })}`, ); assert( secondState.mindmapId === result.mindmapId, `second_browser_mindmap_id_mismatch:${JSON.stringify({ expected: result.mindmapId, actual: secondState.mindmapId })}`, ); screenshots.push(await screenshot(pageB, "03-second-browser-visible")); result.ok = true; result.initialState = initialState; result.afterCommand = afterCommand; result.secondState = secondState; await writeResult(result); } catch (error) { result.error = error instanceof Error ? error.stack || error.message : String(error); screenshots.push(await screenshot(pageA, "99-failure").catch(() => null)); if (pageB) { screenshots.push(await screenshot(pageB, "99-failure-browser-b").catch(() => null)); } await writeResult(result); throw error; } finally { if (contextB) { await contextB.close().catch(() => undefined); } await cleanupDocuments(contextA.request, createdIds).catch(() => undefined); await contextA.close().catch(() => undefined); await browser.close(); } } main().catch((error) => { console.error(error); process.exitCode = 1; });