Files
mnote/scripts/task169-mindmap-realtime-smoke.js

1421 lines
56 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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) => `<<read_response_failed:${error.message}>>`);
}
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("<<read_response_failed:")) {
skippedReadFailures += 1;
continue;
}
failures.push({
code: "mindmap_command_apply_artifact_response_unparseable",
record,
});
continue;
}
parsedCount += 1;
if (eventType.startsWith("tree.resource.")) {
failures.push({
code: "mindmap_command_apply_used_tree_resource_event",
summary,
});
}
if (streamOp === "resync_required") {
failures.push({
code: "mindmap_command_apply_used_tree_resync_delta",
summary,
});
}
}
if (parsedCount === 0) {
failures.push({
code: "mindmap_command_apply_artifact_response_all_unparseable",
skippedReadFailures,
});
}
return { checked: parsedCount, skippedReadFailures, artifacts };
}
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 contextB = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const pageA = await contextA.newPage();
const pageB = await contextB.newPage();
await installTreeEventRecorder(pageA);
await installTreeEventRecorder(pageB);
await installMindmapLoadingRecorder(pageA);
await installMindmapLoadingRecorder(pageB);
const records = [];
attachNetworkCapture(pageA, "browser-a", records);
attachNetworkCapture(pageB, "browser-b", records);
const screenshots = [];
const createdIds = [];
const failures = [];
const result = {
ok: false,
task: TASK,
baseUrl: BASE_URL,
documentId: null,
workspaceId: null,
mindmapId: null,
liveWaitMs: LIVE_WAIT_MS,
failures,
network: records,
screenshots,
};
try {
await ensureAuthenticated(pageA, contextA.request);
await ensureAuthenticated(pageB, contextB.request);
const doc = await createTempDocument(contextA.request, null);
createdIds.push(doc.documentId);
const awayDoc = await createTempDocument(contextA.request, null);
createdIds.push(awayDoc.documentId);
result.documentId = doc.documentId;
result.workspaceId = doc.workspaceId;
await renameDocument(contextA.request, doc.workspaceId, doc.documentId, `task169-mindmap-live-${Date.now().toString().slice(-6)}`);
await renameDocument(contextA.request, awayDoc.workspaceId, awayDoc.documentId, `task169-mindmap-away-${Date.now().toString().slice(-6)}`);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await openDocument(pageB, doc.workspaceId, doc.documentId);
result.browserBBeforeInsert = await readPageState(pageB);
await insertMindmapThroughSlash(pageA);
result.mindmapId = await waitForMindmapId(pageA);
result.documentMindmapBlockAfterInsert = await assertDocumentMindmapBlockUsesReferenceSource(
contextA.request,
doc.documentId,
doc.workspaceId,
result.mindmapId,
failures,
"after-insert",
);
result.fileTreeMindmapOpenAfterInsert = await assertFileTreeMindmapOpenUsesResourceTab(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.fileTreeMindmapRowsAfterInsert = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
null,
failures,
"after-insert",
);
await waitForMindmapReady(pageA, "after-filetree-mindmap-open");
result.resourceTabRoundtripAfterInsert = await assertMindmapResourceTabCanRoundtripWithPageTab(
pageA,
doc.documentId,
result.mindmapId,
failures,
"after-insert",
);
await pageA.waitForTimeout(1000);
screenshots.push(await screenshot(pageA, "01-browser-a-after-insert"));
result.retiredRealtimeTopicEditing = {
status: "skipped",
reason:
"task169 当前收窄为 main editor mindmap resource tab P1 smoke;旧 topic/runtime 实时编辑深测依赖 object shell 时代 runtime 行为,已退役为非必过链路。",
retiredSteps: [
"assertMindmapRuntimeDoesNotRefreshForLiveSignals",
"editTopicTextThroughRuntime",
"enterTopicTextDraftWithoutCommit",
"assertLongLanguageEditSurvivesLiveRefresh",
"assertMindmapCommandApplyArtifactsDoNotTouchTreeResource",
],
};
// 旧实时编辑深测不再作为 task169 的必过主链;下面只保留 resource tab 打开与切换口径。
await openDocument(pageA, awayDoc.workspaceId, awayDoc.documentId);
result.indexOpenAfterResourceTabRoundtrip = await assertFileTreePageMarkdownOpenUsesPageAggregate(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.retiredResourceTabReopenAfterIndex = {
status: "skipped",
reason:
"task169 已收窄为首次 File Tree 打开 mindmap 资源后的 resource tab 断言;二次重开与旧实时深测不属于当前 P1 口径,已退役以免干扰后续 smoke。",
};
const badRecord = findBadRecord(records);
if (badRecord) {
failures.push({ code: "mindmap_network_error_or_validator_leak", badRecord });
}
result.ok = failures.length === 0;
await writeResult(result);
if (!result.ok) {
throw new Error(`mindmap_realtime_failed:${JSON.stringify(failures, null, 2).slice(0, 6000)}`);
}
} catch (error) {
result.error = error instanceof Error ? error.stack || error.message : String(error);
screenshots.push(await screenshot(pageA, "99-failure-browser-a").catch(() => 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;
});