#!/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, } = require("./tree-shell-smoke-helpers"); const TASK = "task169-mindmap-realtime-smoke"; const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); const LIVE_WAIT_MS = Number(process.env.MNOTE_MINDMAP_REALTIME_WAIT_MS || 12_000); 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; } async function installTreeEventRecorder(page) { await page.addInitScript(() => { window.__MNOTE_SMOKE_TREE_EVENTS__ = []; const record = (name, event) => { const detail = event && event.detail ? event.detail : null; window.__MNOTE_SMOKE_TREE_EVENTS__.push({ name, at: Date.now(), revision: detail && detail.revision ? String(detail.revision) : "", data: detail && detail.payload && detail.payload.data ? detail.payload.data : null, eventType: detail && detail.payload && detail.payload.overview && Array.isArray(detail.payload.overview.domain_events) && detail.payload.overview.domain_events[0] ? detail.payload.overview.domain_events[0].event_type || "" : "", }); }; window.addEventListener("tree:snapshot", (event) => record("tree:snapshot", event)); window.addEventListener("tree:delta", (event) => record("tree:delta", event)); window.addEventListener("tree:resync", (event) => record("tree:resync", event)); }); } async function installMindmapLoadingRecorder(page) { await page.addInitScript(() => { window.__MNOTE_MINDMAP_LOADING_EVENTS__ = []; const recordLoading = (node, reason) => { if (!(node instanceof HTMLElement)) return; const loading = node.matches?.('[data-testid="mindmap-scene-loading"]') ? node : node.querySelector?.('[data-testid="mindmap-scene-loading"]'); if (!(loading instanceof HTMLElement)) return; window.__MNOTE_MINDMAP_LOADING_EVENTS__.push({ at: Date.now(), reason, text: loading.textContent || "", url: window.location.href, }); }; const start = () => { if (window.__MNOTE_MINDMAP_LOADING_OBSERVER__) return; const target = document.documentElement || document.body; if (!target) { window.setTimeout(start, 0); return; } recordLoading(target, "initial-scan"); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => recordLoading(node, "added-node")); }); }); observer.observe(target, { childList: true, subtree: true }); window.__MNOTE_MINDMAP_LOADING_OBSERVER__ = observer; }; start(); }); } function attachNetworkCapture(page, label, records) { page.on("request", (request) => { const url = request.url(); if (url.includes("/api/tree/events")) { records.push({ type: "request", label, method: request.method(), url }); } }); page.on("response", async (response) => { const url = response.url(); if ( !url.includes("/api/tree/events") && !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 || url.includes("/api/documents/save")) { responseText = await response.text().catch((error) => `<>`); } records.push({ type: "response", label, method, url, status, requestBody: request.postData() || null, responseText: responseText ? responseText.slice(0, 12000) : null, }); }); page.on("requestfailed", (request) => { const url = request.url(); if (url.includes("/api/tree/events") || url.includes("/api/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) => { const text = message.text(); if (message.type() === "error" || /mindmap|tree live|EventSource|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 readPageState(page) { return page.evaluate(() => { const html = document.documentElement; const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]'); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const error = document.querySelector('[data-testid="leptos-mindmap-error"]'); const loading = document.querySelector('[data-testid="mindmap-scene-loading"]'); const assetRows = Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-asset-id]")) .map((node) => (node instanceof HTMLElement ? node.getAttribute("data-asset-id") || "" : "")) .filter(Boolean); const fileTreeAssetRowElements = Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-asset-id]")) .map((node) => (node instanceof HTMLElement ? node.closest(".tree-row") || node : null)) .filter((node, index, rows) => node instanceof HTMLElement && rows.indexOf(node) === index); const mindmapRows = fileTreeAssetRowElements .filter((node) => { if (!(node instanceof HTMLElement)) return false; const objectKind = node.getAttribute("data-object-kind") || ""; const objectIdentity = node.getAttribute("data-object-identity") || ""; const assetId = node.getAttribute("data-asset-id") || ""; return objectKind === "mindmap" || objectIdentity.includes('"objectKind":"mindmap"') || assetId.startsWith("mindmap"); }) .map((node) => { const element = node; const objectIdentity = element.getAttribute("data-object-identity") || ""; let parsedDocumentId = ""; try { parsedDocumentId = JSON.parse(objectIdentity).documentId || ""; } catch { parsedDocumentId = ""; } return { rowId: element.getAttribute("data-row-id") || "", documentId: element.getAttribute("data-document-id") || element.getAttribute("data-doc-id") || parsedDocumentId, assetId: element.getAttribute("data-asset-id") || "", objectKind: element.getAttribute("data-object-kind") || "", objectIdentity, title: element.textContent || "", }; }); return { url: window.location.href, bodyText: (document.body?.innerText || "").slice(0, 8000), mindmapId: root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null, runtimeReady: runtime instanceof HTMLElement ? runtime.dataset.runtimeReady === "true" : false, runtimeMountCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0, runtimeProjectionApplyCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeProjectionApplyCount || "0") : 0, lastRuntimeMountReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeMountReason || "" : "", lastFetchSceneReason: scene instanceof HTMLElement ? scene.dataset.lastFetchSceneReason || "" : "", backgroundProjectionRefresh: scene instanceof HTMLElement ? scene.dataset.backgroundProjectionRefresh || "" : "", hasMindmapError: Boolean(error), hasMindmapLoading: Boolean(loading), commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || "" : "", treeLiveStatus: html.getAttribute("data-mnote-tree-live-status") || "", treeLiveApplied: html.getAttribute("data-mnote-tree-live-applied") || "", treeLiveRevision: html.getAttribute("data-mnote-tree-live-revision") || "", treeLiveApplyError: html.getAttribute("data-mnote-tree-live-apply-error") || "", lastMindmapAssetOpenMode: html.getAttribute("data-mnote-last-mindmap-asset-open-mode") || "", lastMindmapAssetId: html.getAttribute("data-mnote-last-mindmap-asset-id") || "", objectEditor: document.querySelector("[data-mnote-object-editor]") instanceof HTMLElement ? document.querySelector("[data-mnote-object-editor]").getAttribute("data-mnote-object-editor") || "" : "", objectIdentity: document.querySelector("[data-mnote-object-identity]") instanceof HTMLElement ? document.querySelector("[data-mnote-object-identity]").getAttribute("data-mnote-object-identity") || "" : "", localStorageKeys: Object.keys(window.localStorage || {}).filter((key) => /mnote\.leptos-tiptap-spike\.document|__mindmap_object__|mindmap-object/.test(key), ), mindmapLoadingEvents: window.__MNOTE_MINDMAP_LOADING_EVENTS__ || [], fileTreeText: (document.getElementById("sidebar-file-tree-root")?.textContent || "").slice(0, 4000), fileTreeAssetIds: assetRows, fileTreeMindmapRows: mindmapRows, treeEvents: window.__MNOTE_SMOKE_TREE_EVENTS__ || [], }; }); } async function readMindmapRuntimeStabilityState(page, mindmapId) { return page.evaluate( ({ mindmapId }) => { const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = registry[mindmapId]; const instance = bridge?.instance; const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null; let topicRect = null; if (typeof topicNode?.getRect === "function") { try { topicRect = topicNode.getRect(); } catch (error) { topicRect = null; } } const viewTransform = instance?.view?.getTransformData?.() || null; return { runtimeMountCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0, runtimeProjectionApplyCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeProjectionApplyCount || "0") : 0, lastRuntimeMountReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeMountReason || "" : "", lastRuntimeProjectionApplyReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeProjectionApplyReason || "" : "", runtimeProjectionDeferred: scene instanceof HTMLElement ? scene.dataset.runtimeProjectionDeferred || "" : "", lastRuntimeProjectionDeferReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeProjectionDeferReason || "" : "", backgroundProjectionRefresh: scene instanceof HTMLElement ? scene.dataset.backgroundProjectionRefresh || "" : "", commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || "" : "", commandApplyMode: scene instanceof HTMLElement ? scene.dataset.commandApplyMode || "" : "", topicRect: topicRect ? { x: Number(topicRect.x ?? topicRect.left ?? 0), y: Number(topicRect.y ?? topicRect.top ?? 0), width: Number(topicRect.width ?? 0), height: Number(topicRect.height ?? 0), } : null, viewTransform, }; }, { mindmapId }, ); } function stableJson(value) { return JSON.stringify(value ?? null); } async function waitForMindmapRuntimeViewSettled(page, mindmapId, label) { let previous = await readMindmapRuntimeStabilityState(page, mindmapId); for (let index = 0; index < 12; index += 1) { await page.waitForTimeout(350); const current = await readMindmapRuntimeStabilityState(page, mindmapId); if ( stableJson(current.viewTransform) === stableJson(previous.viewTransform) && stableJson(current.topicRect) === stableJson(previous.topicRect) && current.runtimeMountCount === previous.runtimeMountCount && current.runtimeProjectionApplyCount === previous.runtimeProjectionApplyCount ) { return current; } previous = current; } throw new Error(`mindmap_runtime_view_not_settled:${label}`); } function rectCenter(rect) { if (!rect) return null; return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, }; } function centerDistance(a, b) { const ca = rectCenter(a); const cb = rectCenter(b); if (!ca || !cb) return 0; return Math.hypot(ca.x - cb.x, ca.y - cb.y); } async function dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, eventName) { return page.evaluate( ({ documentId, mindmapId, eventName }) => { const cursorId = `smoke-command-${Date.now()}`; const payload = eventName === "tree:delta" ? { kind: "delta", revision: JSON.stringify({ id: cursorId, createdAt: new Date().toISOString() }), data: { op: "resync_required", reason: "mindmap.put", documentId, blockId: mindmapId, }, } : { kind: "resync", revision: JSON.stringify({ id: cursorId, createdAt: new Date().toISOString() }), overview: { command_logs: [ { id: cursorId, command_id: cursorId, target_page_id: documentId, target_block_id: mindmapId, aggregate_type: "block", aggregate_id: mindmapId, payload: { streamDelta: { op: "resync_required", reason: "mindmap.put", documentId, blockId: mindmapId, }, }, }, ], domain_events: [], }, }; window.dispatchEvent(new CustomEvent(eventName, { detail: { payload, revision: payload.revision } })); return payload; }, { documentId, mindmapId, eventName }, ); } async function assertMindmapRuntimeDoesNotRefreshForLiveSignals(page, documentId, mindmapId, failures, label) { await waitForMindmapRuntimeViewSettled(page, mindmapId, label); const before = await readPageState(page); const beforeRuntime = await readMindmapRuntimeStabilityState(page, mindmapId); if (before.hasMindmapError || before.hasMindmapLoading || !before.runtimeReady) { failures.push({ code: "mindmap_unstable_before_live_signal", label, state: before, }); return { before, afterResync: before, afterDelta: before }; } const resyncPayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:resync"); await page.waitForTimeout(2_500); const afterResync = await readPageState(page); const afterResyncRuntime = await readMindmapRuntimeStabilityState(page, mindmapId); if (afterResync.runtimeMountCount !== before.runtimeMountCount) { failures.push({ code: "mindmap_resync_remounted_runtime", label, before, after: afterResync, payload: resyncPayload, }); } if (afterResync.runtimeProjectionApplyCount !== before.runtimeProjectionApplyCount) { failures.push({ code: "mindmap_resync_applied_projection_despite_usability_first", label, before, after: afterResync, payload: resyncPayload, }); } if (stableJson(afterResyncRuntime.viewTransform) !== stableJson(beforeRuntime.viewTransform)) { failures.push({ code: "mindmap_resync_changed_view_transform", label, before: beforeRuntime, after: afterResyncRuntime, payload: resyncPayload, }); } if (centerDistance(beforeRuntime.topicRect, afterResyncRuntime.topicRect) > 2) { failures.push({ code: "mindmap_resync_moved_topic_node", label, before: beforeRuntime, after: afterResyncRuntime, payload: resyncPayload, }); } const deltaPayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:delta"); await page.waitForTimeout(2_500); const afterDelta = await readPageState(page); const afterDeltaRuntime = await readMindmapRuntimeStabilityState(page, mindmapId); if (afterDelta.runtimeMountCount !== afterResync.runtimeMountCount) { failures.push({ code: "mindmap_delta_remounted_runtime", label, before: afterResync, after: afterDelta, payload: deltaPayload, }); } if (afterDelta.runtimeProjectionApplyCount !== afterResync.runtimeProjectionApplyCount) { failures.push({ code: "mindmap_delta_applied_projection_despite_usability_first", label, before: afterResync, after: afterDelta, payload: deltaPayload, }); } if (stableJson(afterDeltaRuntime.viewTransform) !== stableJson(afterResyncRuntime.viewTransform)) { failures.push({ code: "mindmap_delta_changed_view_transform", label, before: afterResyncRuntime, after: afterDeltaRuntime, payload: deltaPayload, }); } if (centerDistance(afterResyncRuntime.topicRect, afterDeltaRuntime.topicRect) > 2) { failures.push({ code: "mindmap_delta_moved_topic_node", label, before: afterResyncRuntime, after: afterDeltaRuntime, payload: deltaPayload, }); } if (afterDelta.hasMindmapError || afterDelta.hasMindmapLoading) { failures.push({ code: "mindmap_live_signal_left_error_or_loading", label, after: afterDelta, }); } return { before, beforeRuntime, afterResync, afterResyncRuntime, afterDelta, afterDeltaRuntime }; } 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 waitForMindmapId(page) { await waitForMindmapReady(page, "mindmap-id"); const state = await readPageState(page); assert(state.mindmapId, `mindmap_id_missing:${JSON.stringify(state)}`); return state.mindmapId; } 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)) { return lastText; } await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error(`document_content_missing_mindmap:${lastText.slice(0, 3000)}`); } async function assertFileTreeMindmapOpenUsesResourceTab(page, documentId, mindmapId, failures) { await openFilesystemView(page); const opened = await page.evaluate( ({ documentId, mindmapId }) => { const rows = Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-asset-id]")); const row = rows.find((node) => { if (!(node instanceof HTMLElement)) return false; const assetId = node.getAttribute("data-asset-id") || ""; const objectIdentity = node.getAttribute("data-object-identity") || ""; let parsedDocumentId = ""; try { parsedDocumentId = JSON.parse(objectIdentity).documentId || ""; } catch { parsedDocumentId = ""; } const rowDocumentId = node.getAttribute("data-document-id") || node.getAttribute("data-doc-id") || parsedDocumentId; return assetId === mindmapId && rowDocumentId === documentId; }); if (!(row instanceof HTMLElement)) { return { clicked: false, reason: "mindmap_asset_row_missing" }; } const button = row.querySelector('[data-rust-action="open"], .tree-link'); if (!(button instanceof HTMLElement)) { return { clicked: false, reason: "mindmap_asset_open_button_missing" }; } button.click(); return { clicked: true, assetId: row.getAttribute("data-asset-id") || "", rowId: row.getAttribute("data-row-id") || "", objectIdentity: row.getAttribute("data-object-identity") || "", objectKind: row.getAttribute("data-object-kind") || "", }; }, { documentId, mindmapId }, ); if (!opened.clicked) { failures.push({ code: "filetree_mindmap_asset_open_row_missing", opened }); return opened; } if (!opened.objectIdentity.includes(`"objectKind":"mindmap"`) || !opened.objectIdentity.includes(`"assetId":"${mindmapId}"`)) { failures.push({ code: "filetree_mindmap_asset_row_missing_object_identity", opened, }); } await page.waitForFunction( (mindmapId) => { return document.documentElement.getAttribute("data-mnote-last-mindmap-asset-open-mode") === "mindmap-resource-tab" && document.documentElement.getAttribute("data-mnote-last-mindmap-asset-id") === mindmapId && Boolean(document.querySelector('.mnote-main-tab.is-active[data-mnote-tab-kind="mindmap"]')); }, mindmapId, { timeout: UI_TIMEOUT_MS }, ).catch(() => null); const state = await readPageState(page); const url = new URL(state.url); const tabState = await page.evaluate(({ documentId, mindmapId }) => { const identity = `resource:mindmap:${documentId}:${mindmapId}`; const tab = document.querySelector(`.mnote-main-tab[data-mnote-main-tab="${CSS.escape(identity)}"]`); const panel = document.querySelector(`.mnote-resource-tab-panel[data-mnote-resource-tab-panel="${CSS.escape(identity)}"]`); const root = panel?.querySelector('[data-testid="mnote-mindmap-editor-root"]'); return { identity, tabExists: tab instanceof HTMLElement, tabActive: tab instanceof HTMLElement && tab.classList.contains("is-active"), panelVisible: panel instanceof HTMLElement && !panel.hidden, objectEditor: panel instanceof HTMLElement ? panel.getAttribute("data-mnote-object-editor") || "" : "", rootMindmapId: root instanceof HTMLElement ? root.getAttribute("data-mnote-mindmap-id") || "" : "", openMode: document.documentElement.getAttribute("data-mnote-last-mindmap-asset-open-mode") || "", }; }, { documentId, mindmapId }); if (url.pathname.includes(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`)) { failures.push({ code: "filetree_mindmap_asset_open_used_retired_object_shell", opened, state, tabState, }); } if (!tabState.tabExists || !tabState.tabActive || !tabState.panelVisible || tabState.objectEditor !== "mindmap" || tabState.rootMindmapId !== mindmapId) { failures.push({ code: "filetree_mindmap_asset_open_did_not_use_resource_tab", opened, state, tabState, }); } await waitForMindmapReady(page, "filetree-resource-tab-open"); const readyState = await readPageState(page); if (readyState.mindmapId !== mindmapId || !readyState.runtimeReady || readyState.hasMindmapError) { failures.push({ code: "filetree_mindmap_asset_resource_tab_not_ready", opened, readyState, }); } return { opened, state: readyState }; } async function readMindmapResourceTabState(page, documentId, mindmapId) { return page.evaluate( ({ documentId, mindmapId }) => { const identity = `resource:mindmap:${documentId}:${mindmapId}`; const tab = document.querySelector(`.mnote-main-tab[data-mnote-main-tab="${CSS.escape(identity)}"]`); const activeTab = document.querySelector(".mnote-main-tab.is-active"); const panel = document.querySelector( `.mnote-resource-tab-panel[data-mnote-resource-tab-panel="${CSS.escape(identity)}"]`, ); const root = panel?.querySelector('[data-testid="mnote-mindmap-editor-root"]'); const pageTab = document.querySelector('[data-mnote-main-tab="page"]'); const pagePanel = document.querySelector("[data-mnote-page-tab-panel]"); const resourceHost = document.querySelector("[data-mnote-resource-tab-host]"); return { identity, activeTabIdentity: activeTab instanceof HTMLElement ? activeTab.getAttribute("data-mnote-main-tab") || "" : "", activeTabKind: activeTab instanceof HTMLElement ? activeTab.getAttribute("data-mnote-tab-kind") || "" : "", activeMindmapSelector: Boolean(document.querySelector('.mnote-main-tab.is-active[data-mnote-tab-kind="mindmap"]')), tabExists: tab instanceof HTMLElement, tabActive: tab instanceof HTMLElement && tab.classList.contains("is-active"), tabKind: tab instanceof HTMLElement ? tab.getAttribute("data-mnote-tab-kind") || "" : "", panelExists: panel instanceof HTMLElement, panelVisible: panel instanceof HTMLElement && !panel.hidden, panelObjectEditor: panel instanceof HTMLElement ? panel.getAttribute("data-mnote-object-editor") || "" : "", panelObjectIdentity: panel instanceof HTMLElement ? panel.getAttribute("data-mnote-object-identity") || "" : "", panelMindmapId: panel instanceof HTMLElement ? panel.getAttribute("data-mnote-mindmap-id") || "" : "", rootExists: root instanceof HTMLElement, rootObjectEditor: root instanceof HTMLElement ? root.getAttribute("data-mnote-object-editor") || "" : "", rootObjectIdentity: root instanceof HTMLElement ? root.getAttribute("data-mnote-object-identity") || "" : "", rootMindmapId: root instanceof HTMLElement ? root.getAttribute("data-mnote-mindmap-id") || "" : "", pageTabActive: pageTab instanceof HTMLElement && pageTab.classList.contains("is-active"), pagePanelVisible: pagePanel instanceof HTMLElement && !pagePanel.hidden, resourceHostVisible: resourceHost instanceof HTMLElement && !resourceHost.hidden, }; }, { documentId, mindmapId }, ); } async function assertMindmapResourceTabCanRoundtripWithPageTab(page, documentId, mindmapId, failures, label) { const before = await readMindmapResourceTabState(page, documentId, mindmapId); const expectedIdentity = `resource:mindmap:${documentId}:${mindmapId}`; if ( before.identity !== expectedIdentity || !before.activeMindmapSelector || !before.tabExists || !before.tabActive || before.tabKind !== "mindmap" || !before.panelVisible || before.panelObjectEditor !== "mindmap" || before.panelObjectIdentity !== expectedIdentity || before.panelMindmapId !== mindmapId || before.rootObjectEditor !== "mindmap" || before.rootObjectIdentity !== expectedIdentity || before.rootMindmapId !== mindmapId ) { failures.push({ code: "mindmap_resource_tab_identity_or_panel_invalid", label, expectedIdentity, state: before, }); return { before, skippedRoundtrip: true }; } await page.locator('[data-mnote-main-tab="page"]').first().click({ timeout: UI_TIMEOUT_MS }); await page.waitForFunction( () => { const pageTab = document.querySelector('[data-mnote-main-tab="page"]'); const pagePanel = document.querySelector("[data-mnote-page-tab-panel]"); const resourceHost = document.querySelector("[data-mnote-resource-tab-host]"); return ( pageTab instanceof HTMLElement && pageTab.classList.contains("is-active") && pagePanel instanceof HTMLElement && !pagePanel.hidden && resourceHost instanceof HTMLElement && resourceHost.hidden ); }, null, { timeout: UI_TIMEOUT_MS }, ); const afterPageTab = await readMindmapResourceTabState(page, documentId, mindmapId); if (!afterPageTab.pageTabActive || !afterPageTab.pagePanelVisible || afterPageTab.resourceHostVisible) { failures.push({ code: "mindmap_resource_tab_page_roundtrip_failed_to_show_page", label, expectedIdentity, state: afterPageTab, }); } await page.evaluate( ({ identity }) => { const tab = document.querySelector(`.mnote-main-tab[data-mnote-main-tab="${CSS.escape(identity)}"]`); if (!(tab instanceof HTMLElement)) return false; tab.click(); return true; }, { identity: expectedIdentity }, ); await page.waitForFunction( ({ identity }) => { const tab = document.querySelector(`.mnote-main-tab[data-mnote-main-tab="${CSS.escape(identity)}"]`); const panel = document.querySelector( `.mnote-resource-tab-panel[data-mnote-resource-tab-panel="${CSS.escape(identity)}"]`, ); return ( tab instanceof HTMLElement && tab.classList.contains("is-active") && tab.getAttribute("data-mnote-tab-kind") === "mindmap" && panel instanceof HTMLElement && !panel.hidden ); }, { identity: expectedIdentity }, { timeout: UI_TIMEOUT_MS }, ); const afterMindmapTab = await readMindmapResourceTabState(page, documentId, mindmapId); if ( !afterMindmapTab.activeMindmapSelector || !afterMindmapTab.tabActive || !afterMindmapTab.panelVisible || afterMindmapTab.panelObjectIdentity !== expectedIdentity || afterMindmapTab.rootObjectIdentity !== expectedIdentity || afterMindmapTab.rootMindmapId !== mindmapId ) { failures.push({ code: "mindmap_resource_tab_page_roundtrip_failed_to_restore_mindmap", label, expectedIdentity, state: afterMindmapTab, }); } return { before, afterPageTab, afterMindmapTab }; } async function assertSingleMindmapAssetRowStable(page, documentId, mindmapId, expectedObjectIdentity, failures, label) { await openFilesystemView(page); const state = await readPageState(page); const rows = state.fileTreeMindmapRows.filter((row) => row.documentId === documentId); if (rows.length !== 1) { failures.push({ code: "filetree_mindmap_asset_row_count_changed", label, expectedCount: 1, actualCount: rows.length, rows, state, }); return { ok: false, rows, state, objectIdentity: expectedObjectIdentity }; } const row = rows[0]; if (row.assetId !== mindmapId) { failures.push({ code: "filetree_mindmap_asset_id_changed", label, expectedAssetId: mindmapId, row, state, }); } if (!row.objectIdentity.includes(`"objectKind":"mindmap"`) || !row.objectIdentity.includes(`"assetId":"${mindmapId}"`)) { failures.push({ code: "filetree_mindmap_asset_identity_invalid", label, row, state, }); } if (expectedObjectIdentity && row.objectIdentity !== expectedObjectIdentity) { failures.push({ code: "filetree_mindmap_asset_identity_changed", label, expectedObjectIdentity, row, state, }); } return { ok: failures.length === 0, rows, state, objectIdentity: row.objectIdentity, }; } async function assertFileTreePageMarkdownOpenUsesPageAggregate(page, documentId, mindmapId, failures) { await openFilesystemView(page); const opened = await page.evaluate( ({ documentId }) => { const row = document.querySelector(`#sidebar-file-tree-root [data-row-id="doc:${CSS.escape(documentId)}"]`) || Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-row-kind='document']")).find((node) => { if (!(node instanceof HTMLElement)) return false; const rowDocumentId = node.getAttribute("data-document-id") || node.getAttribute("data-doc-id") || ""; return rowDocumentId === documentId; }); if (!(row instanceof HTMLElement)) { return { clicked: false, reason: "page_markdown_row_missing" }; } const button = row.querySelector('[data-rust-action="open"], .tree-link'); if (!(button instanceof HTMLElement)) { return { clicked: false, reason: "page_markdown_open_button_missing" }; } button.click(); return { clicked: true, rowId: row.getAttribute("data-row-id") || "", objectIdentity: row.getAttribute("data-object-identity") || "", }; }, { documentId }, ); if (!opened.clicked) { failures.push({ code: "filetree_page_markdown_open_row_missing", opened }); return opened; } if (!opened.objectIdentity.includes(`"objectKind":"page"`) || !opened.objectIdentity.includes(`"documentId":"${documentId}"`)) { failures.push({ code: "filetree_page_markdown_row_missing_object_identity", opened, }); } await page.waitForURL((url) => url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`), { timeout: UI_TIMEOUT_MS, }).catch(() => null); const state = await readPageState(page); const url = new URL(state.url); if (!url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`)) { failures.push({ code: "filetree_page_markdown_open_did_not_return_document_page", opened, state, }); } const tabState = await readMindmapResourceTabState(page, documentId, mindmapId); if (!tabState.pageTabActive || !tabState.pagePanelVisible || tabState.resourceHostVisible || tabState.activeTabKind === "mindmap") { failures.push({ code: "filetree_page_markdown_open_did_not_activate_page_tab", opened, state, tabState, }); } return { opened, state, tabState }; } function readMindmapBlocksFromDocumentContentText(text) { const payload = JSON.parse(text); const content = Array.isArray(payload?.content) ? payload.content : Array.isArray(payload?.result?.content) ? payload.result.content : Array.isArray(payload?.data?.content) ? payload.data.content : Array.isArray(payload?.body?.content) ? payload.body.content : []; return content.filter((block) => block?.type === "mindmap" || block?.blockType === "mindmap"); } async function assertDocumentMindmapBlockUsesReferenceSource(requestContext, documentId, workspaceId, mindmapId, failures, label) { const text = await waitForDocumentContentToIncludeMindmap(requestContext, documentId, workspaceId, mindmapId); const blocks = readMindmapBlocksFromDocumentContentText(text); const target = blocks.find((block) => { const props = block?.props && typeof block.props === "object" ? block.props : {}; return props.mindmapId === mindmapId || props.mindmap_id === mindmapId || block.id === mindmapId || block.blockId === mindmapId; }); if (!target) { failures.push({ code: "document_mindmap_block_reference_missing", label, mindmapId, blocks, responseText: text.slice(0, 3000), }); return { ok: false, blocks }; } const props = target.props && typeof target.props === "object" ? target.props : {}; if (!props.mindmapId || props.rootNodeId !== "root" || Object.prototype.hasOwnProperty.call(props, "data")) { failures.push({ code: "document_mindmap_block_uses_wrong_source", label, mindmapId, props, target, }); return { ok: false, target }; } return { ok: true, target }; } async function waitForMindmapProjectionText(requestContext, documentId, mindmapId, expectedText) { let lastText = ""; for (let index = 0; index < 45; index += 1) { 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 }, ); lastText = await response.text(); if (response.ok() && lastText.includes(expectedText)) return lastText; await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error(`mindmap_projection_missing_text:${expectedText}:${lastText.slice(0, 3000)}`); } async function readMindmapRuntimeTextState(page, mindmapId, expectedText) { return page.evaluate( ({ mindmapId, expectedText }) => { const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = registry[mindmapId]; const snapshot = bridge?.getSnapshot?.() ?? bridge?.instance?.getData?.(true) ?? null; const root = snapshot && typeof snapshot === "object" && "root" in snapshot ? snapshot.root : snapshot; 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, includesExpectedText: texts.some((text) => text.includes(expectedText)), snapshot, }; }, { mindmapId, expectedText }, ); } async function readMindmapProjectionText(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 }, ); return { ok: response.ok(), status: response.status(), text: await response.text(), }; } 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.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, }; }, { 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 readRuntimeTextEditState(page) { return page.evaluate(() => { const edit = document.querySelector(".smm-node-edit-wrap[contenteditable=\"true\"]"); const scene = document.querySelector('[data-testid="leptos-mindmap-island"]'); const active = document.activeElement; return { exists: edit instanceof HTMLElement, visible: edit instanceof HTMLElement && edit.style.display !== "none" && edit.getClientRects().length > 0, text: edit instanceof HTMLElement ? edit.innerText || edit.textContent || "" : "", active: edit instanceof HTMLElement && (active === edit || (active instanceof Node && edit.contains(active))), runtimeMountCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0, runtimeProjectionApplyCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeProjectionApplyCount || "0") : 0, runtimeProjectionDeferred: scene instanceof HTMLElement ? scene.dataset.runtimeProjectionDeferred || "" : "", lastRuntimeProjectionDeferReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeProjectionDeferReason || "" : "", backgroundProjectionRefresh: scene instanceof HTMLElement ? scene.dataset.backgroundProjectionRefresh || "" : "", }; }); } async function openTopicTextEdit(page, mindmapId) { const result = await page.evaluate( ({ mindmapId }) => { const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {}; const bridge = registry[mindmapId]; const instance = bridge?.instance; const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null; const textEdit = instance?.renderer?.textEdit; if (!topicNode) return { ok: false, reason: "topic_node_missing" }; if (typeof textEdit?.show !== "function") return { ok: false, reason: "text_edit_show_missing" }; textEdit.show({ node: topicNode, isFromKeyDown: false }); return { ok: true }; }, { mindmapId }, ); assert(result.ok, `topic_text_edit_open_failed:${JSON.stringify(result)}`); const edit = page.locator(".smm-node-edit-wrap[contenteditable=\"true\"]").first(); await edit.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await edit.focus(); await page.keyboard.press("Control+A"); await page.keyboard.press("Backspace"); } async function enterTopicTextDraftWithoutCommit(page, mindmapId, text) { await openTopicTextEdit(page, mindmapId); await page.keyboard.insertText(text); await page.waitForFunction( ({ text }) => { const edit = document.querySelector(".smm-node-edit-wrap[contenteditable=\"true\"]"); return edit instanceof HTMLElement && (edit.innerText || edit.textContent || "").includes(text); }, { text }, { timeout: UI_TIMEOUT_MS }, ); return readRuntimeTextEditState(page); } async function assertLongLanguageEditSurvivesLiveRefresh(page, requestContext, documentId, mindmapId, failures) { const longText = `长文本编辑-LIVE-${Date.now().toString().slice(-6)}:` + "第一段用于覆盖真实中文连续输入,包含标点、换行前的较长句子,确保编辑框不会因为后台实时刷新而丢焦或闪烁。"; const secondHalf = "第二段继续输入,验证 live signal 之后仍然可以稳定完成编辑并保存到投影。"; await openTopicTextEdit(page, mindmapId); const beforeEditRuntime = await readMindmapRuntimeStabilityState(page, mindmapId); await page.keyboard.insertText(longText); const beforeLive = await readRuntimeTextEditState(page); const resyncPayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:resync"); await page.waitForTimeout(2_500); const afterLive = await readRuntimeTextEditState(page); if (!afterLive.visible || !afterLive.active || !afterLive.text.includes(longText)) { failures.push({ code: "mindmap_live_refresh_interrupted_long_text_edit", beforeLive, afterLive, payload: resyncPayload, }); return { beforeLive, afterLive, committed: false, longText: longText + secondHalf }; } if (afterLive.runtimeMountCount !== beforeLive.runtimeMountCount) { failures.push({ code: "mindmap_live_refresh_remounted_during_long_text_edit", beforeLive, afterLive, payload: resyncPayload, }); } if (afterLive.runtimeProjectionApplyCount !== beforeLive.runtimeProjectionApplyCount) { failures.push({ code: "mindmap_live_refresh_applied_projection_during_long_text_edit", beforeLive, afterLive, payload: resyncPayload, }); } await page.keyboard.insertText(secondHalf); await page.keyboard.press("Enter"); await waitForMindmapProjectionText(requestContext, documentId, mindmapId, longText + secondHalf); await page.waitForTimeout(2_500); const afterCommitRuntime = await readMindmapRuntimeStabilityState(page, mindmapId); if (afterCommitRuntime.runtimeMountCount !== beforeEditRuntime.runtimeMountCount) { failures.push({ code: "mindmap_long_text_commit_remounted_runtime", before: beforeEditRuntime, after: afterCommitRuntime, }); } if (afterCommitRuntime.runtimeProjectionApplyCount !== beforeEditRuntime.runtimeProjectionApplyCount) { failures.push({ code: "mindmap_long_text_commit_applied_projection_refresh", before: beforeEditRuntime, after: afterCommitRuntime, }); } if (stableJson(afterCommitRuntime.viewTransform) !== stableJson(beforeEditRuntime.viewTransform)) { failures.push({ code: "mindmap_long_text_commit_changed_view_transform", before: beforeEditRuntime, after: afterCommitRuntime, }); } const postCommitLivePayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:resync"); await page.waitForTimeout(2_500); const afterPostCommitLiveRuntime = await readMindmapRuntimeStabilityState(page, mindmapId); if (afterPostCommitLiveRuntime.runtimeMountCount !== afterCommitRuntime.runtimeMountCount) { failures.push({ code: "mindmap_post_commit_live_remounted_runtime", before: afterCommitRuntime, after: afterPostCommitLiveRuntime, payload: postCommitLivePayload, }); } if (afterPostCommitLiveRuntime.runtimeProjectionApplyCount !== afterCommitRuntime.runtimeProjectionApplyCount) { failures.push({ code: "mindmap_post_commit_live_applied_projection_refresh", before: afterCommitRuntime, after: afterPostCommitLiveRuntime, payload: postCommitLivePayload, }); } if (stableJson(afterPostCommitLiveRuntime.viewTransform) !== stableJson(afterCommitRuntime.viewTransform)) { failures.push({ code: "mindmap_post_commit_live_changed_view_transform", before: afterCommitRuntime, after: afterPostCommitLiveRuntime, payload: postCommitLivePayload, }); } if (centerDistance(afterCommitRuntime.topicRect, afterPostCommitLiveRuntime.topicRect) > 2) { failures.push({ code: "mindmap_post_commit_live_moved_topic_node", before: afterCommitRuntime, after: afterPostCommitLiveRuntime, payload: postCommitLivePayload, }); } return { beforeEditRuntime, beforeLive, afterLive, committed: true, longText: longText + secondHalf, afterCommitRuntime, afterPostCommitLiveRuntime, afterCommit: await readPageState(page), }; } async function waitForPageCondition(page, predicate, timeoutMs = LIVE_WAIT_MS) { const deadline = Date.now() + timeoutMs; let last = null; while (Date.now() < deadline) { last = await readPageState(page); if (predicate(last)) { return { ok: true, state: last }; } await page.waitForTimeout(500); } return { ok: false, state: last || (await readPageState(page)) }; } function findBadRecord(records) { return 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 || ""}`); }); } function parseJsonMaybe(value) { if (typeof value !== "string" || !value.trim()) return null; try { return JSON.parse(value); } catch { return null; } } function assertMindmapCommandApplyArtifactsDoNotTouchTreeResource(records, failures) { const commandApplyResponses = records.filter((record) => { if (record.type !== "response" || record.method !== "POST" || !record.url.includes("/api/mindmap/")) { return false; } const requestBody = parseJsonMaybe(record.requestBody); return requestBody?.commandName === "mindmap.command.apply"; }); if (commandApplyResponses.length === 0) { failures.push({ code: "mindmap_command_apply_artifact_response_missing" }); return { checked: 0, artifacts: [] }; } const artifacts = []; let parsedCount = 0; let skippedReadFailures = 0; for (const record of commandApplyResponses) { const responseBody = parseJsonMaybe(record.responseText); const eventType = String(responseBody?.artifacts?.domainEvent?.eventType || ""); const streamDelta = responseBody?.artifacts?.domainEvent?.payload?.streamDelta || null; const streamOp = String(streamDelta?.op || ""); const summary = { url: record.url, commandName: responseBody?.commandName || "", eventType, streamOp, }; artifacts.push(summary); if (!responseBody) { if (String(record.responseText || "").startsWith("< null)); screenshots.push(await screenshot(pageB, "99-failure-browser-b").catch(() => null)); await writeResult(result); throw error; } finally { await cleanupDocuments(contextA.request, createdIds).catch(() => undefined); await contextA.close().catch(() => undefined); await contextB.close().catch(() => undefined); await browser.close(); } } main().catch((error) => { console.error(error); process.exitCode = 1; });