#!/usr/bin/env node "use strict"; const fs = require("node:fs/promises"); const fsSync = require("node:fs"); 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"); const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; const PROBE_DOCX_PATH = process.env.MNOTE_ONLYOFFICE_PROBE_DOCX || "/tmp/mnote-onlyoffice-manual/mnote-onlyoffice-upload-test.docx"; 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 uploadOfficeAsset(request, workspaceId, documentId) { assert(fsSync.existsSync(PROBE_DOCX_PATH), `缺少探测文件:${PROBE_DOCX_PATH}`); const buffer = await fs.readFile(PROBE_DOCX_PATH); const response = await request.fetch(`${BASE_URL}/api/media/upload`, { method: "POST", multipart: { file: { name: "task456-office.docx", mimeType: DOCX_MIME, buffer, }, workspaceId, documentId, }, timeout: UI_TIMEOUT_MS, }); const payload = await response.json().catch(async () => ({ raw: await response.text() })); assert(response.ok(), `/api/media/upload 请求失败:${response.status()} ${JSON.stringify(payload)}`); const asset = payload && payload.asset && typeof payload.asset === "object" ? payload.asset : null; const assetId = asset && typeof asset.id === "string" ? asset.id : ""; assert(assetId, `上传结果缺少 asset.id:${JSON.stringify(payload)}`); return asset; } 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, officeId: officeAssetId }, ); } async function readObjectShellState(page) { return await page.evaluate(() => ({ currentObjectEditor: document.querySelector("[data-mnote-object-editor]")?.getAttribute("data-mnote-object-editor") || "", currentObjectIdentity: document.querySelector("[data-mnote-object-identity]")?.getAttribute("data-mnote-object-identity") || "", currentUrl: window.location.href, })); } async function readFileTreeProjection(request, workspaceId, rootNodeId) { return await requestJson( request, `/api/tree/projections/file?workspaceId=${encodeURIComponent(workspaceId)}&rootNodeId=${encodeURIComponent(rootNodeId)}&depth=3`, { method: "GET" }, ); } (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}`; await createMindmap(context.request, doc.workspaceId, doc.documentId, mindmapId, `TEST-456-mind-${stamp}`); const officeAsset = await uploadOfficeAsset(context.request, doc.workspaceId, doc.documentId); const officeAssetId = officeAsset.id; result.fixture = { ...doc, title, mindmapId, officeAssetId, officeAsset }; result.projectionAfterUpload = await readFileTreeProjection(context.request, doc.workspaceId, doc.documentId).catch((error) => ({ error: error instanceof Error ? error.message : String(error), })); 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 page.waitForSelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${officeAssetId}"]`, { timeout: UI_TIMEOUT_MS }); await page.evaluate(() => { window.__task456FileRoot = document.getElementById("sidebar-file-tree-root"); }); result.before = await readResourceState(page, doc.documentId, mindmapId, officeAssetId); result.beforeFileTreeRows = await page.evaluate(() => Array.from(document.querySelectorAll("#sidebar-file-tree-root .tree-row[data-shell-mode='filetree']")).map((row) => ({ rowId: row.getAttribute("data-row-id") || "", rowKind: row.getAttribute("data-row-kind") || "", documentId: row.getAttribute("data-document-id") || "", ownerDocumentId: row.getAttribute("data-owner-document-id") || "", assetId: row.getAttribute("data-asset-id") || "", objectKind: row.getAttribute("data-object-kind") || "", title: row.querySelector(".tree-link-title")?.textContent?.trim() || "", })), ); 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 page.waitForSelector(`#sidebar-file-tree-root .tree-row[data-asset-id="${officeAssetId}"]`, { timeout: UI_TIMEOUT_MS }); result.afterMindmapUpdate = await readResourceState(page, doc.documentId, mindmapId, officeAssetId); assert(result.afterMindmapUpdate.mindmapExists, "mindmap 更新后 filetree 资源行不应丢失"); assert(result.afterMindmapUpdate.pageExists, "mindmap 更新后页面行不应丢失"); const officePopupPromise = page.waitForEvent("popup", { timeout: UI_TIMEOUT_MS }); await page.click(`#sidebar-file-tree-root .tree-row[data-asset-id="${officeAssetId}"] .tree-link`, { timeout: UI_TIMEOUT_MS }); const officePage = await officePopupPromise; await officePage.waitForLoadState("domcontentloaded", { timeout: UI_TIMEOUT_MS }); await officePage.waitForSelector("[data-mnote-object-editor='onlyoffice']", { timeout: UI_TIMEOUT_MS }); result.afterOfficeOpen = await readObjectShellState(officePage); 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); });