收口工作台资源 tab 与本地文件树 P1
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -139,6 +139,15 @@ async function main() {
|
||||
await page.waitForFunction(() => document.documentElement.getAttribute("data-mnote-filetree-bulk-delete-applied") === "true", null, {
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
const actionState = await page.evaluate(() => ({
|
||||
action: document.documentElement.getAttribute("data-mnote-filetree-last-action"),
|
||||
status: document.documentElement.getAttribute("data-mnote-filetree-last-action-status"),
|
||||
rowId: document.documentElement.getAttribute("data-mnote-filetree-last-action-row-id"),
|
||||
applied: document.documentElement.getAttribute("data-mnote-filetree-bulk-delete-applied"),
|
||||
}));
|
||||
assert.equal(actionState.action, "bulk-delete", `bulk delete 应记录 action 名: ${JSON.stringify(actionState)}`);
|
||||
assert.equal(actionState.status, "archived", `bulk delete 应记录 undo/archive 状态: ${JSON.stringify(actionState)}`);
|
||||
assert.equal(actionState.rowId, rowIds[1], `bulk delete 应记录触发目标 row: ${JSON.stringify(actionState)}`);
|
||||
for (const asset of assets) {
|
||||
await page.locator(`#sidebar-file-tree-root .tree-row[data-asset-id="${asset.assetId}"]`).waitFor({
|
||||
state: "detached",
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const ACTOR_ID = "user_real";
|
||||
|
||||
function fileUrl(localPath) {
|
||||
return `file://${localPath}`;
|
||||
}
|
||||
|
||||
function writeWorkspaceManifest(root) {
|
||||
fs.mkdirSync(path.join(root, ".mnote"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(root, ".mnote", "workspace.json"),
|
||||
`${JSON.stringify({
|
||||
workspaceId: "local-ws:user_real:task472",
|
||||
ownerId: ACTOR_ID,
|
||||
createdAt: new Date().toISOString(),
|
||||
capabilities: ["local_files", "tree_commands", "markdown_edit"],
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function requestJson(requestPath, init = {}) {
|
||||
const response = await fetch(`${BASE_URL}${requestPath}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init.body ? { "content-type": "application/json" } : {}),
|
||||
"x-mnote-actor-id": ACTOR_ID,
|
||||
"x-mnote-actor-type": "user",
|
||||
...(init.headers || {}),
|
||||
},
|
||||
});
|
||||
const text = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
payload = text;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`${requestPath} 请求失败: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function readFileTreeProjection(rootUri) {
|
||||
const url = new URL(`${BASE_URL}/api/tree/projections/file`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", rootUri);
|
||||
const payload = await requestJson(`${url.pathname}${url.search}`);
|
||||
const result = payload.result || payload;
|
||||
assert.equal(result.sourceKind, "local_folder", `projection 应来自 local_folder: ${JSON.stringify(result)}`);
|
||||
assert.equal(result.projection, "file_tree", `projection 类型应为 file_tree: ${JSON.stringify(result)}`);
|
||||
assert(Array.isArray(result.items), `projection items 应为数组: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function rootMarkdownOrder(projection) {
|
||||
return projection.items
|
||||
.filter((item) => item && item.parentNodeId == null && item.rowKind === "markdown")
|
||||
.map((item) => item.resourceMeta?.extra?.source?.relativePath || "");
|
||||
}
|
||||
|
||||
function readPersistedOrder(root) {
|
||||
const orderPath = path.join(root, ".mnote", "file-order.json");
|
||||
assert(fs.existsSync(orderPath), "move sortOrder 后应写入 .mnote/file-order.json");
|
||||
return JSON.parse(fs.readFileSync(orderPath, "utf8"));
|
||||
}
|
||||
|
||||
async function moveWithSortOrder(rootUri, documentId, sortOrder) {
|
||||
const payload = await requestJson("/api/tree/commands", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
action: "move",
|
||||
sourceKind: "local_folder",
|
||||
rootUri,
|
||||
documentId,
|
||||
parentId: null,
|
||||
sortOrder,
|
||||
}),
|
||||
});
|
||||
assert.equal(payload.result?.execution?._unsupportedFields, undefined, `sortOrder 不应再落入 _unsupportedFields: ${JSON.stringify(payload)}`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task472-sort-order-"));
|
||||
const rootUri = fileUrl(root);
|
||||
|
||||
try {
|
||||
writeWorkspaceManifest(root);
|
||||
fs.writeFileSync(path.join(root, "alpha.md"), "# Alpha\n", "utf8");
|
||||
fs.writeFileSync(path.join(root, "beta.md"), "# Beta\n", "utf8");
|
||||
fs.writeFileSync(path.join(root, "source.md"), "# Source\n", "utf8");
|
||||
|
||||
const initialProjection = await readFileTreeProjection(rootUri);
|
||||
assert.deepEqual(
|
||||
rootMarkdownOrder(initialProjection).slice(0, 3),
|
||||
["alpha.md", "beta.md", "source.md"],
|
||||
"初始 projection 应按文件系统自然顺序作为基线",
|
||||
);
|
||||
|
||||
await moveWithSortOrder(rootUri, "local-md:source.md", 0);
|
||||
|
||||
const immediateProjection = await readFileTreeProjection(rootUri);
|
||||
assert.deepEqual(
|
||||
rootMarkdownOrder(immediateProjection).slice(0, 3),
|
||||
["source.md", "alpha.md", "beta.md"],
|
||||
"move sortOrder 后立即重新加载 projection 应保持新顺序",
|
||||
);
|
||||
|
||||
const persistedOrder = readPersistedOrder(root);
|
||||
assert(JSON.stringify(persistedOrder).includes("source.md"), `file-order 应记录 source.md: ${JSON.stringify(persistedOrder)}`);
|
||||
|
||||
const reloadedProjection = await readFileTreeProjection(rootUri);
|
||||
assert.deepEqual(
|
||||
rootMarkdownOrder(reloadedProjection).slice(0, 3),
|
||||
["source.md", "alpha.md", "beta.md"],
|
||||
"再次重新加载 projection 顺序不应回退到自然排序",
|
||||
);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
root,
|
||||
rootUri,
|
||||
order: rootMarkdownOrder(reloadedProjection).slice(0, 3),
|
||||
persistedOrder,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
if (!process.env.MNOTE_KEEP_SMOKE_TMP) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
|
||||
function fileUrl(localPath) {
|
||||
return `file://${localPath}`;
|
||||
}
|
||||
|
||||
function localMdDocumentId(relativePath) {
|
||||
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
||||
}
|
||||
|
||||
function documentUrl(root, relativePath, secondaryRelativePath = "") {
|
||||
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", fileUrl(root));
|
||||
url.searchParams.set("treeView", "filetree");
|
||||
if (secondaryRelativePath) {
|
||||
url.searchParams.set("secondaryDocumentId", localMdDocumentId(secondaryRelativePath));
|
||||
url.searchParams.set("secondarySourceKind", "local_folder");
|
||||
url.searchParams.set("secondaryRootUri", fileUrl(root));
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function writeWorkspaceManifest(root, ownerId) {
|
||||
const metadataDir = path.join(root, ".mnote");
|
||||
fs.mkdirSync(metadataDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(metadataDir, "workspace.json"),
|
||||
`${JSON.stringify({
|
||||
workspaceId: `local-ws:${ownerId}:task472`,
|
||||
ownerId,
|
||||
createdAt: new Date().toISOString(),
|
||||
capabilities: ["local_files", "markdown_edit", "asset_upload"],
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function quickLogin(page) {
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" });
|
||||
if (await quickLoginButton.count()) {
|
||||
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => url.pathname === "/", { timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadLocalAsset(page, root, documentId, fileName, mimeType, bytes, kind) {
|
||||
return await page.evaluate(
|
||||
async ({ rootUri, documentId, fileName, mimeType, bytes, kind }) => {
|
||||
const form = new FormData();
|
||||
form.append("rootUri", rootUri);
|
||||
form.append("documentId", documentId);
|
||||
form.append("kind", kind);
|
||||
form.append("file", new File([new Uint8Array(bytes)], fileName, { type: mimeType }));
|
||||
const response = await fetch("/api/local-folder/assets/upload", { method: "POST", body: form });
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(`upload_failed_${response.status}:${JSON.stringify(payload)}`);
|
||||
}
|
||||
return payload.asset;
|
||||
},
|
||||
{ rootUri: fileUrl(root), documentId, fileName, mimeType, bytes: Array.from(bytes), kind },
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForPrimaryReady(page) {
|
||||
await page.locator(".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror").first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForSecondaryDocument(page, expectedDocumentId) {
|
||||
await page.waitForFunction(
|
||||
({ expected }) => {
|
||||
const pane = document.querySelector('.document-pane[data-pane-role="secondary"]');
|
||||
const editor = pane?.querySelector(".editor-surface .ProseMirror");
|
||||
return pane instanceof HTMLElement
|
||||
&& pane.getAttribute("data-pane-visible") === "true"
|
||||
&& pane.getAttribute("data-pane-document-id") === expected
|
||||
&& editor instanceof HTMLElement
|
||||
&& editor.isContentEditable;
|
||||
},
|
||||
{ expected: expectedDocumentId },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function readSideTargetState(page) {
|
||||
return await page.evaluate(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const pane = document.querySelector('.document-pane[data-pane-role="secondary"]');
|
||||
const activeTab = document.querySelector(".mnote-main-tab.is-active");
|
||||
const placeholder = document.querySelector("[data-mnote-side-target-placeholder=\"true\"]");
|
||||
return {
|
||||
resourceTab: url.searchParams.get("resourceTab") || "",
|
||||
secondaryDocumentId: url.searchParams.get("secondaryDocumentId"),
|
||||
secondarySourceKind: url.searchParams.get("secondarySourceKind"),
|
||||
secondaryRootUri: url.searchParams.get("secondaryRootUri"),
|
||||
secondaryVisible: pane instanceof HTMLElement && pane.getAttribute("data-pane-visible") === "true" && !pane.hidden,
|
||||
secondaryDocumentDomId: pane?.getAttribute("data-pane-document-id") || "",
|
||||
secondarySideTarget: pane?.getAttribute("data-mnote-side-target") || "",
|
||||
activeTabKind: activeTab?.getAttribute("data-mnote-tab-kind") || "",
|
||||
activeTabText: activeTab?.textContent || "",
|
||||
placeholderText: placeholder?.textContent || "",
|
||||
unsupportedFlag: document.documentElement.getAttribute("data-mnote-side-target-unsupported") || "",
|
||||
popupCount: window.__mnoteSideTargetPopupCount || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task472-side-target-"));
|
||||
const relativePath = "README.md";
|
||||
const firstSideRelativePath = "Side.md";
|
||||
const secondSideRelativePath = "Second.md";
|
||||
const documentId = localMdDocumentId(relativePath);
|
||||
const firstSideDocumentId = localMdDocumentId(firstSideRelativePath);
|
||||
const secondSideDocumentId = localMdDocumentId(secondSideRelativePath);
|
||||
writeWorkspaceManifest(root, "user_real");
|
||||
fs.writeFileSync(path.join(root, relativePath), "# Page\n\n主页面\n", "utf8");
|
||||
fs.writeFileSync(path.join(root, firstSideRelativePath), "# Side\n\n第一侧栏\n", "utf8");
|
||||
fs.writeFileSync(path.join(root, secondSideRelativePath), "# Second\n\n第二侧栏\n", "utf8");
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: CHROMIUM_EXECUTABLE_PATH,
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1360, height: 900 },
|
||||
extraHTTPHeaders: {
|
||||
"x-mnote-actor-id": "user_real",
|
||||
"x-mnote-actor-type": "user",
|
||||
},
|
||||
});
|
||||
await context.addInitScript(() => {
|
||||
window.__mnoteSideTargetPopupCount = 0;
|
||||
const originalOpen = window.open;
|
||||
window.open = function(...args) {
|
||||
window.__mnoteSideTargetPopupCount += 1;
|
||||
return originalOpen.apply(window, args);
|
||||
};
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await quickLogin(page);
|
||||
await page.goto(documentUrl(root, relativePath, firstSideRelativePath), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await waitForPrimaryReady(page);
|
||||
await waitForSecondaryDocument(page, firstSideDocumentId);
|
||||
|
||||
const asset = await uploadLocalAsset(
|
||||
page,
|
||||
root,
|
||||
documentId,
|
||||
"side-target-resource.md",
|
||||
"text/markdown",
|
||||
Buffer.from("# Resource\n\n资源正文\n", "utf8"),
|
||||
"attachment",
|
||||
);
|
||||
|
||||
await page.evaluate(({ asset, documentId }) => {
|
||||
window.dispatchEvent(new CustomEvent("tree.asset.open", {
|
||||
detail: {
|
||||
assetId: asset.id,
|
||||
documentId,
|
||||
title: asset.file_name || "side-target-resource.md",
|
||||
assetType: asset.asset_type || "attachment",
|
||||
},
|
||||
}));
|
||||
}, { asset, documentId });
|
||||
await page.locator('.mnote-main-tab.is-active[data-mnote-tab-kind="markdown"]').waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
const activeResourceBeforeSide = await readSideTargetState(page);
|
||||
assert(activeResourceBeforeSide.resourceTab.includes("side-target-resource.md"), `active resource tab 应写入 URL: ${JSON.stringify(activeResourceBeforeSide)}`);
|
||||
assert.equal(activeResourceBeforeSide.secondaryDocumentId, firstSideDocumentId, `资源 tab 不应清理既有 secondaryDocumentId: ${JSON.stringify(activeResourceBeforeSide)}`);
|
||||
|
||||
await page.evaluate(({ documentId }) => {
|
||||
window.dispatchEvent(new CustomEvent("tree.page.open", {
|
||||
detail: {
|
||||
documentId,
|
||||
openTarget: "side",
|
||||
},
|
||||
}));
|
||||
}, { documentId: secondSideDocumentId });
|
||||
await waitForSecondaryDocument(page, secondSideDocumentId);
|
||||
const afterDocumentSideOpen = await readSideTargetState(page);
|
||||
assert.equal(afterDocumentSideOpen.secondaryDocumentId, secondSideDocumentId, `document openTarget=side 应更新 secondaryDocumentId: ${JSON.stringify(afterDocumentSideOpen)}`);
|
||||
assert.equal(afterDocumentSideOpen.secondarySourceKind, "local_folder", `document openTarget=side 应维护 secondarySourceKind: ${JSON.stringify(afterDocumentSideOpen)}`);
|
||||
assert.equal(afterDocumentSideOpen.secondaryRootUri, fileUrl(root), `document openTarget=side 应维护 secondaryRootUri: ${JSON.stringify(afterDocumentSideOpen)}`);
|
||||
assert(afterDocumentSideOpen.resourceTab.includes("side-target-resource.md"), `更新 secondary pane 不应清空 active resource tab URL: ${JSON.stringify(afterDocumentSideOpen)}`);
|
||||
assert.equal(afterDocumentSideOpen.activeTabKind, "markdown", `active resource tab 与 secondary pane 应同时存在: ${JSON.stringify(afterDocumentSideOpen)}`);
|
||||
|
||||
await page.locator('[data-mnote-pane-close="secondary"]').first().click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForTimeout(250);
|
||||
const afterCloseSecondary = await readSideTargetState(page);
|
||||
assert.equal(afterCloseSecondary.secondaryDocumentId, null, `关闭 secondary pane 应清理 secondaryDocumentId: ${JSON.stringify(afterCloseSecondary)}`);
|
||||
assert.equal(afterCloseSecondary.secondarySourceKind, null, `关闭 secondary pane 应清理 secondarySourceKind: ${JSON.stringify(afterCloseSecondary)}`);
|
||||
assert.equal(afterCloseSecondary.secondaryRootUri, null, `关闭 secondary pane 应清理 secondaryRootUri: ${JSON.stringify(afterCloseSecondary)}`);
|
||||
assert(afterCloseSecondary.resourceTab.includes("side-target-resource.md"), `关闭 secondary pane 不应清空 active resource tab URL: ${JSON.stringify(afterCloseSecondary)}`);
|
||||
assert.equal(afterCloseSecondary.activeTabKind, "markdown", `关闭 secondary pane 不应切走 active resource tab: ${JSON.stringify(afterCloseSecondary)}`);
|
||||
|
||||
await page.evaluate(({ asset, documentId }) => {
|
||||
window.dispatchEvent(new CustomEvent("tree.asset.open", {
|
||||
detail: {
|
||||
assetId: asset.id,
|
||||
documentId,
|
||||
title: asset.file_name || "side-target-resource.md",
|
||||
assetType: asset.asset_type || "attachment",
|
||||
openTarget: "side",
|
||||
},
|
||||
}));
|
||||
}, { asset, documentId });
|
||||
await page.waitForFunction(() => document.querySelector("[data-mnote-side-target-placeholder=\"true\"]"), {}, { timeout: UI_TIMEOUT_MS });
|
||||
const afterResourceSideOpen = await readSideTargetState(page);
|
||||
assert.equal(afterResourceSideOpen.secondaryDocumentId, null, `资源 openTarget=side placeholder 不应保留旧 secondaryDocumentId: ${JSON.stringify(afterResourceSideOpen)}`);
|
||||
assert.equal(afterResourceSideOpen.secondarySideTarget, "unsupported-resource", `资源 openTarget=side 应标记 unsupported side target: ${JSON.stringify(afterResourceSideOpen)}`);
|
||||
assert.match(afterResourceSideOpen.placeholderText, /暂不支持在侧栏打开此资源/, `资源 side placeholder 应可观测: ${JSON.stringify(afterResourceSideOpen)}`);
|
||||
assert(afterResourceSideOpen.resourceTab.includes("side-target-resource.md"), `资源 side placeholder 不应清空 active resource tab URL: ${JSON.stringify(afterResourceSideOpen)}`);
|
||||
assert.equal(afterResourceSideOpen.activeTabKind, "markdown", `资源 side placeholder 不应切走 active resource tab: ${JSON.stringify(afterResourceSideOpen)}`);
|
||||
assert.equal(afterResourceSideOpen.popupCount, 0, `资源 openTarget=side 不应误开新窗口: ${JSON.stringify(afterResourceSideOpen)}`);
|
||||
|
||||
console.log(JSON.stringify({ ok: true, root, assetId: asset.id }, null, 2));
|
||||
} finally {
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error && error.stack ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user