Files
mnote/scripts/task456-resource-object-shell-sync-smoke.js
T

238 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 readOnlyOfficePageState(page) {
return await page.evaluate(() => ({
currentUrl: window.location.href,
ready: Boolean(window.__MNOTE_ONLYOFFICE_READY__),
editorExists: Boolean(window.__MNOTE_ONLYOFFICE_EDITOR__),
frameCount: document.querySelectorAll("#onlyoffice-frame iframe, #onlyoffice-frame canvas").length,
errorVisible: document.getElementById("onlyoffice-error")?.getAttribute("data-visible") || "",
debug: window.__MNOTE_ONLYOFFICE_DEBUG__ || null,
errorLog: Array.isArray(window.__MNOTE_ONLYOFFICE_ERRLOG__) ? window.__MNOTE_ONLYOFFICE_ERRLOG__.slice() : [],
}));
}
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.waitForURL((url) => url.pathname === "/onlyoffice", { timeout: UI_TIMEOUT_MS });
await officePage.waitForFunction(
() => Boolean(window.__MNOTE_ONLYOFFICE_READY__) || Boolean(window.__MNOTE_ONLYOFFICE_EDITOR__),
{ timeout: UI_TIMEOUT_MS },
);
result.afterOfficeOpen = await readOnlyOfficePageState(officePage);
assert(new URL(result.afterOfficeOpen.currentUrl).pathname === "/onlyoffice", "office 应打开 OnlyOffice 页面");
assert(result.afterOfficeOpen.ready || result.afterOfficeOpen.editorExists, "OnlyOffice 页面应完成初始化或持有编辑器实例");
assert(result.afterOfficeOpen.debug && result.afterOfficeOpen.debug.assetId === officeAssetId, "OnlyOffice 页面应携带当前资源 assetId");
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);
});