1421 lines
56 KiB
JavaScript
1421 lines
56 KiB
JavaScript
#!/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;
|
||
});
|