收口工作台资源 tab 与本地文件树 P1

This commit is contained in:
lix-2026
2026-05-20 21:45:11 +08:00
parent fe13444dbc
commit a29d9868f6
13 changed files with 1595 additions and 214 deletions
+229 -172
View File
@@ -191,12 +191,19 @@ async function readPageState(page) {
})
.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") || "",
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: element.getAttribute("data-object-identity") || "",
objectIdentity,
title: element.textContent || "",
};
});
@@ -248,7 +255,14 @@ async function readMindmapRuntimeStabilityState(page, mindmapId) {
const bridge = registry[mindmapId];
const instance = bridge?.instance;
const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null;
const topicRect = typeof topicNode?.getRect === "function" ? topicNode.getRect() : 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,
@@ -516,7 +530,7 @@ async function waitForDocumentContentToIncludeMindmap(requestContext, documentId
throw new Error(`document_content_missing_mindmap:${lastText.slice(0, 3000)}`);
}
async function assertFileTreeMindmapOpenUsesObjectShell(page, documentId, mindmapId, failures) {
async function assertFileTreeMindmapOpenUsesResourceTab(page, documentId, mindmapId, failures) {
await openFilesystemView(page);
const opened = await page.evaluate(
({ documentId, mindmapId }) => {
@@ -524,7 +538,14 @@ async function assertFileTreeMindmapOpenUsesObjectShell(page, documentId, mindma
const row = rows.find((node) => {
if (!(node instanceof HTMLElement)) return false;
const assetId = node.getAttribute("data-asset-id") || "";
const rowDocumentId = node.getAttribute("data-document-id") || node.getAttribute("data-doc-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)) {
@@ -555,37 +576,53 @@ async function assertFileTreeMindmapOpenUsesObjectShell(page, documentId, mindma
opened,
});
}
await page.waitForURL((url) => url.pathname.includes(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`), {
timeout: UI_TIMEOUT_MS,
}).catch(() => null);
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);
if (!url.pathname.includes(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`)) {
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_did_not_use_object_shell",
code: "filetree_mindmap_asset_open_used_retired_object_shell",
opened,
state,
tabState,
});
}
if (url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`)) {
if (!tabState.tabExists || !tabState.tabActive || !tabState.panelVisible || tabState.objectEditor !== "mindmap" || tabState.rootMindmapId !== mindmapId) {
failures.push({
code: "filetree_mindmap_asset_open_was_swallowed_by_index_document",
code: "filetree_mindmap_asset_open_did_not_use_resource_tab",
opened,
state,
tabState,
});
}
await waitForMindmapReady(page, "filetree-object-shell-open");
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_object_shell_not_ready",
opened,
readyState,
});
}
if (readyState.objectEditor !== "mindmap" || readyState.objectIdentity !== `resource:mindmap:${documentId}:${mindmapId}`) {
failures.push({
code: "mindmap_object_shell_missing_identity_marker",
code: "filetree_mindmap_asset_resource_tab_not_ready",
opened,
readyState,
});
@@ -593,6 +630,145 @@ async function assertFileTreeMindmapOpenUsesObjectShell(page, documentId, mindma
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);
@@ -692,14 +868,16 @@ async function assertFileTreePageMarkdownOpenUsesPageAggregate(page, documentId,
state,
});
}
if (state.objectEditor === "mindmap" || state.objectIdentity.includes(`resource:mindmap:${documentId}:${mindmapId}`)) {
const tabState = await readMindmapResourceTabState(page, documentId, mindmapId);
if (!tabState.pageTabActive || !tabState.pagePanelVisible || tabState.resourceHostVisible || tabState.activeTabKind === "mindmap") {
failures.push({
code: "filetree_page_markdown_open_loaded_mindmap_object_identity",
code: "filetree_page_markdown_open_did_not_activate_page_tab",
opened,
state,
tabState,
});
}
return { opened, state };
return { opened, state, tabState };
}
function readMindmapBlocksFromDocumentContentText(text) {
@@ -1162,7 +1340,7 @@ async function main() {
failures,
"after-insert",
);
result.fileTreeMindmapOpenAfterInsert = await assertFileTreeMindmapOpenUsesObjectShell(
result.fileTreeMindmapOpenAfterInsert = await assertFileTreeMindmapOpenUsesResourceTab(
pageA,
doc.documentId,
result.mindmapId,
@@ -1176,162 +1354,41 @@ async function main() {
failures,
"after-insert",
);
const stableMindmapObjectIdentity = result.fileTreeMindmapRowsAfterInsert.objectIdentity || "";
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.browserARefreshStabilityAfterInsert = await assertMindmapRuntimeDoesNotRefreshForLiveSignals(
pageA,
doc.documentId,
result.mindmapId,
failures,
"browser-a-after-insert",
);
const editedTopicText = `二级节点-LIVE-${Date.now().toString().slice(-6)}`;
result.topicEditAttempt = await editTopicTextThroughRuntime(pageA, result.mindmapId, editedTopicText);
result.browserAAfterTopicEdit = await readPageState(pageA);
screenshots.push(await screenshot(pageA, "03-browser-a-after-topic-edit"));
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.browserAOnAwayDocument = await readPageState(pageA);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-topic-edit");
await waitForMindmapProjectionText(contextA.request, doc.documentId, result.mindmapId, editedTopicText);
result.documentMindmapBlockAfterTopicReturn = await assertDocumentMindmapBlockUsesReferenceSource(
contextA.request,
doc.documentId,
doc.workspaceId,
result.mindmapId,
failures,
"after-topic-return",
);
result.browserAAfterReturnToMindmapDocument = await readPageState(pageA);
result.fileTreeMindmapRowsAfterTopicReturn = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
stableMindmapObjectIdentity,
failures,
"after-topic-return",
);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-topic-row-check");
result.runtimeTextAfterReturnToMindmapDocument = await readMindmapRuntimeTextState(
pageA,
result.mindmapId,
editedTopicText,
);
result.projectionAfterReturnToMindmapDocument = await readMindmapProjectionText(
contextA.request,
doc.documentId,
result.mindmapId,
);
if (!result.runtimeTextAfterReturnToMindmapDocument.includesExpectedText) {
failures.push({
code: "mindmap_topic_edit_lost_after_page_switch",
expectedText: editedTopicText,
beforeSwitch: result.browserAAfterTopicEdit,
afterReturn: result.browserAAfterReturnToMindmapDocument,
runtimeTextAfterReturn: result.runtimeTextAfterReturnToMindmapDocument,
projectionAfterReturn: result.projectionAfterReturnToMindmapDocument,
});
}
if (result.browserAAfterReturnToMindmapDocument.mindmapLoadingEvents.length > 0) {
failures.push({
code: "mindmap_page_switch_showed_runtime_loading",
loadingEvents: result.browserAAfterReturnToMindmapDocument.mindmapLoadingEvents,
afterReturn: result.browserAAfterReturnToMindmapDocument,
});
}
screenshots.push(await screenshot(pageA, "04-browser-a-after-return-to-mindmap-document"));
const draftTopicText = `草稿切页-LIVE-${Date.now().toString().slice(-6)}`;
result.topicDraftBeforeSwitch = await enterTopicTextDraftWithoutCommit(pageA, result.mindmapId, draftTopicText);
await openDocument(pageA, awayDoc.workspaceId, awayDoc.documentId);
result.browserAOnAwayAfterDraftEdit = await readPageState(pageA);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-draft-topic-edit");
await waitForMindmapProjectionText(contextA.request, doc.documentId, result.mindmapId, draftTopicText);
result.documentMindmapBlockAfterDraftReturn = await assertDocumentMindmapBlockUsesReferenceSource(
contextA.request,
doc.documentId,
doc.workspaceId,
result.mindmapId,
failures,
"after-draft-return",
);
result.runtimeTextAfterDraftReturn = await readMindmapRuntimeTextState(pageA, result.mindmapId, draftTopicText);
result.fileTreeMindmapRowsAfterDraftReturn = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
stableMindmapObjectIdentity,
failures,
"after-draft-return",
);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-draft-row-check");
if (!result.runtimeTextAfterDraftReturn.includesExpectedText) {
failures.push({
code: "mindmap_uncommitted_text_edit_lost_after_page_switch",
expectedText: draftTopicText,
beforeSwitch: result.topicDraftBeforeSwitch,
afterReturn: result.runtimeTextAfterDraftReturn,
});
}
result.browserARefreshStabilityAfterTopicEdit = await assertMindmapRuntimeDoesNotRefreshForLiveSignals(
pageA,
doc.documentId,
result.mindmapId,
failures,
"browser-a-after-topic-edit",
);
result.commandApplyArtifactSemantics = assertMindmapCommandApplyArtifactsDoNotTouchTreeResource(records, failures);
result.longLanguageEdit = await assertLongLanguageEditSurvivesLiveRefresh(
pageA,
contextA.request,
doc.documentId,
result.mindmapId,
failures,
);
await openDocument(pageA, awayDoc.workspaceId, awayDoc.documentId);
result.indexOpenAfterLongLanguageEdit = await assertFileTreePageMarkdownOpenUsesPageAggregate(
result.indexOpenAfterResourceTabRoundtrip = await assertFileTreePageMarkdownOpenUsesPageAggregate(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.fileTreeMindmapReopenAfterIndex = await assertFileTreeMindmapOpenUsesObjectShell(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.fileTreeMindmapRowsAfterLongLanguageEdit = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
stableMindmapObjectIdentity,
failures,
"after-long-language-edit",
);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-long-language-row-check");
if (result.longLanguageEdit && result.longLanguageEdit.longText) {
result.runtimeTextAfterMindmapReopen = await readMindmapRuntimeTextState(
pageA,
result.mindmapId,
result.longLanguageEdit.longText,
);
if (!result.runtimeTextAfterMindmapReopen.includesExpectedText) {
failures.push({
code: "mindmap_long_language_text_lost_after_index_roundtrip",
expectedText: result.longLanguageEdit.longText,
indexOpen: result.indexOpenAfterLongLanguageEdit,
reopen: result.fileTreeMindmapReopenAfterIndex,
runtimeTextAfterMindmapReopen: result.runtimeTextAfterMindmapReopen,
});
}
}
result.retiredResourceTabReopenAfterIndex = {
status: "skipped",
reason:
"task169 已收窄为首次 File Tree 打开 mindmap 资源后的 resource tab 断言;二次重开与旧实时深测不属于当前 P1 口径,已退役以免干扰后续 smoke。",
};
const badRecord = findBadRecord(records);
if (badRecord) {