#!/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, requestJson, } = require("./tree-shell-smoke-helpers"); const TASK = "task456-resource-object-shell-sync-smoke"; const OUT_DIR = path.join(process.cwd(), "tmp", TASK); const RESULT_PATH = path.join(OUT_DIR, "result.json"); async function writeResult(result) { await fs.mkdir(OUT_DIR, { recursive: true }); await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...result, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8"); } async function createMindmap(request, workspaceId, documentId, mindmapId, title) { return await requestJson(request, `/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, { method: "POST", data: { commandName: "mindmaps.put", workspaceId, createOnly: true, data: { data: { text: title, uid: "root" }, children: [] }, }, }); } async function readResourceState(page, documentId, mindmapId, officeAssetId) { return await page.evaluate( ({ documentId: docId, mindmapId: mapId, officeId }) => { const fileRoot = document.getElementById("sidebar-file-tree-root"); const mindmapRow = document.querySelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${CSS.escape(mapId)}"]`); const officeRow = document.querySelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${CSS.escape(officeId)}"]`); const pageRow = document.querySelector(`#sidebar-file-tree-root .tree-row[data-row-id="doc:${CSS.escape(docId)}"]`); const mindmapTitle = mindmapRow?.querySelector(":scope > .tree-link > .tree-link-title")?.textContent || ""; const officeTitle = officeRow?.querySelector(":scope > .tree-link > .tree-link-title")?.textContent || ""; return { fileRootStable: Boolean(fileRoot && fileRoot === window.__task456FileRoot), pageExists: pageRow instanceof HTMLElement, mindmapExists: mindmapRow instanceof HTMLElement, officeExists: officeRow instanceof HTMLElement, mindmapSelected: mindmapRow instanceof HTMLElement ? mindmapRow.getAttribute("data-selected") || "" : "", officeSelected: officeRow instanceof HTMLElement ? officeRow.getAttribute("data-selected") || "" : "", mindmapTitle: mindmapTitle.trim(), officeTitle: officeTitle.trim(), mindmapObjectIdentity: mindmapRow instanceof HTMLElement ? mindmapRow.dataset.objectIdentity || "" : "", officeObjectIdentity: officeRow instanceof HTMLElement ? officeRow.dataset.objectIdentity || "" : "", currentObjectEditor: document.querySelector("[data-mnote-object-editor]")?.getAttribute("data-mnote-object-editor") || "", currentObjectIdentity: document.querySelector("[data-mnote-object-identity]")?.getAttribute("data-mnote-object-identity") || "", }; }, { documentId, mindmapId, officeAssetId }, ); } async function injectOfficeRow(page, documentId, assetId) { await page.evaluate( ({ docId, officeId }) => { const root = document.getElementById("sidebar-file-tree-root"); if (!(root instanceof HTMLElement)) throw new Error("缺少 file tree root"); if (document.querySelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${CSS.escape(officeId)}"]`)) return; const row = document.createElement("div"); row.className = "tree-row"; row.dataset.testid = "filetree-asset-row"; row.dataset.assetId = officeId; row.dataset.objectKind = "onlyoffice"; row.dataset.objectIdentity = JSON.stringify({ objectKind: "onlyoffice", documentId: docId, assetId: officeId }); row.innerHTML = ``; row.querySelector("button").addEventListener("click", () => { window.location.href = `/office/${encodeURIComponent(docId)}/${encodeURIComponent(officeId)}?fileName=task456-office.docx&fileType=docx&mode=edit`; }); root.appendChild(row); }, { docId: documentId, officeId: assetId }, ); } (async () => { await fs.mkdir(OUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1" }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await context.newPage(); const createdDocuments = []; const result = { baseUrl: BASE_URL, fixture: {}, before: null, afterMindmapOpen: null, afterMindmapUpdate: null, afterOfficeOpen: null, failures: [], }; try { await ensureAuthenticated(page, context.request); const doc = await createTempDocument(context.request, null); createdDocuments.push(doc.documentId); const stamp = Date.now().toString().slice(-8); const title = `TEST-456-resource-${stamp}`; await renameDocument(context.request, doc.workspaceId, doc.documentId, title); const mindmapId = `mindmap_456_${stamp}`; const officeAssetId = `asset_office_456_${stamp}`; await createMindmap(context.request, doc.workspaceId, doc.documentId, mindmapId, `TEST-456-mind-${stamp}`); result.fixture = { ...doc, title, mindmapId, officeAssetId }; await openDocument(page, doc.workspaceId, doc.documentId); await openFilesystemView(page); await page.waitForSelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${mindmapId}"]`, { timeout: UI_TIMEOUT_MS }); await injectOfficeRow(page, doc.documentId, officeAssetId); await page.evaluate(() => { window.__task456FileRoot = document.getElementById("sidebar-file-tree-root"); }); result.before = await readResourceState(page, doc.documentId, mindmapId, officeAssetId); assert(result.before.mindmapExists, "filetree 应显示 mindmap 资源行"); assert(result.before.officeExists, "filetree 应显示 office 资源行"); await page.click(`#sidebar-file-tree-root .tree-row[data-asset-id="${mindmapId}"] .tree-link`, { timeout: UI_TIMEOUT_MS }); await page.waitForURL((url) => url.pathname.includes(`/mindmap/${encodeURIComponent(doc.documentId)}/${encodeURIComponent(mindmapId)}`), { timeout: UI_TIMEOUT_MS, }); await page.waitForSelector("[data-mnote-object-editor='mindmap']", { timeout: UI_TIMEOUT_MS }); result.afterMindmapOpen = await readResourceState(page, doc.documentId, mindmapId, officeAssetId); assert(result.afterMindmapOpen.fileRootStable, "打开 mindmap 后 filetree root 不应被替换"); assert(result.afterMindmapOpen.currentObjectIdentity.includes(`resource:mindmap:${doc.documentId}:${mindmapId}`), "mindmap 对象壳应暴露 resource identity"); await requestJson(context.request, `/api/mindmap/${encodeURIComponent(doc.documentId)}/${encodeURIComponent(mindmapId)}`, { method: "POST", data: { commandName: "mindmap.command.apply", workspaceId: doc.workspaceId, commands: [{ type: "updateText", mindmapId, nodeId: "root", text: `TEST-456-updated-${stamp}` }], projectionRevision: 1, }, }); await openDocument(page, doc.workspaceId, doc.documentId); await openFilesystemView(page); await injectOfficeRow(page, doc.documentId, officeAssetId); result.afterMindmapUpdate = await readResourceState(page, doc.documentId, mindmapId, officeAssetId); assert(result.afterMindmapUpdate.mindmapExists, "mindmap 更新后 filetree 资源行不应丢失"); assert(result.afterMindmapUpdate.pageExists, "mindmap 更新后页面行不应丢失"); await page.click(`#sidebar-file-tree-root .tree-row[data-asset-id="${officeAssetId}"] .tree-link`, { timeout: UI_TIMEOUT_MS }); await page.waitForURL((url) => url.pathname.includes(`/office/${encodeURIComponent(doc.documentId)}/${encodeURIComponent(officeAssetId)}`), { timeout: UI_TIMEOUT_MS, }); await page.waitForSelector("[data-mnote-object-editor='onlyoffice']", { timeout: UI_TIMEOUT_MS }); result.afterOfficeOpen = await readResourceState(page, doc.documentId, mindmapId, officeAssetId); assert(result.afterOfficeOpen.currentObjectIdentity.includes(`resource:onlyoffice:${doc.documentId}:${officeAssetId}`), "office 对象壳应暴露 resource identity"); await writeResult({ ...result, ok: true, finalUrl: page.url() }); console.log(`ok ${TASK} ${RESULT_PATH}`); } catch (error) { await writeResult({ ...result, ok: false, currentUrl: page.url(), error: error instanceof Error ? error.stack || error.message : String(error), }); throw error; } finally { if (createdDocuments.length) await cleanupDocuments(context.request, createdDocuments).catch(() => null); await context.close().catch(() => undefined); await browser.close().catch(() => undefined); } })().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : error); process.exit(1); });