Files
mnote/scripts/task174-rust-onlyoffice-attachment-open-smoke.js
T

268 lines
9.5 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.
"use strict";
// 说明:
// - 验证 Rust 3000 主入口的 Convex 附件行打开 OnlyOffice 链路。
// - 脚本会创建临时页面、上传临时 docx、点击文件树附件行并断言新窗口打开 /onlyoffice。
// - 结束后清理临时页面,避免污染长期测试空间。
const fs = require("node:fs");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
assert,
createTempDocument,
ensureAuthenticated,
openDocument,
openFilesystemView,
purgeDocument,
} = require("./tree-shell-smoke-helpers");
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
const ONLYOFFICE_READY_TIMEOUT_MS = Number(process.env.MNOTE_ONLYOFFICE_READY_TIMEOUT_MS || 120_000);
const PROBE_DOCX_PATH =
process.env.MNOTE_ONLYOFFICE_PROBE_DOCX ||
"/tmp/mnote-onlyoffice-manual/mnote-onlyoffice-upload-test.docx";
const TEST_EMAIL = "mnote.e2e@example.com";
const TEST_PASSWORD = "MnoteE2E123!";
let authCookieHeader = "";
const popupConsoleMessages = [];
const popupNetworkFailures = [];
function parseSetCookie(setCookie, origin) {
const [nameValue] = String(setCookie || "").split(";");
const separator = nameValue.indexOf("=");
if (separator <= 0) return null;
return {
name: nameValue.slice(0, separator).trim(),
value: nameValue.slice(separator + 1).trim(),
domain: new URL(origin).hostname,
path: "/",
httpOnly: /;\s*httponly\b/i.test(setCookie),
sameSite: "Lax",
};
}
async function forceTestAccountLogin(context) {
const response = await context.request.fetch(`${BASE_URL}/api/auth`, {
method: "POST",
data: {
action: "auth:signIn",
args: {
provider: "password",
params: {
email: TEST_EMAIL,
password: TEST_PASSWORD,
flow: "signIn",
},
},
},
headers: { "content-type": "application/json" },
timeout: UI_TIMEOUT_MS,
});
const payload = await response.json().catch(async () => ({ raw: await response.text() }));
assert(response.ok(), `测试账号登录失败:${response.status()} ${JSON.stringify(payload)}`);
const cookies = response
.headersArray()
.filter((header) => header.name.toLowerCase() === "set-cookie")
.map((header) => parseSetCookie(header.value, BASE_URL))
.filter(Boolean);
assert(cookies.length > 0, "测试账号登录响应缺少 set-cookie");
await context.addCookies(cookies);
authCookieHeader = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
}
async function uploadProbeDocx(requestContext, target) {
assert(fs.existsSync(PROBE_DOCX_PATH), `缺少探测文件:${PROBE_DOCX_PATH}`);
const buffer = fs.readFileSync(PROBE_DOCX_PATH);
const response = await requestContext.fetch(`${BASE_URL}/api/media/upload`, {
method: "POST",
headers: authCookieHeader ? { cookie: authCookieHeader } : undefined,
multipart: {
file: {
name: "task174-onlyoffice-attachment.docx",
mimeType: DOCX_MIME,
buffer,
},
workspaceId: target.workspaceId,
documentId: target.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)}`,
);
assert(
payload && payload.asset && typeof payload.asset.id === "string",
`上传结果缺少 asset.id${JSON.stringify(payload)}`,
);
return payload.asset;
}
async function waitForAssetRow(page, assetId) {
const selector = `[data-testid="filetree-asset-row"][data-asset-id="${assetId}"]`;
const row = page.locator(selector).first();
await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return row;
}
async function waitForOnlyOfficeReady(page) {
page.on("console", (message) => {
popupConsoleMessages.push({
type: message.type(),
text: message.text().slice(0, 1000),
});
});
page.on("pageerror", (error) => {
popupConsoleMessages.push({
type: "pageerror",
text: error.message.slice(0, 1000),
});
});
page.on("response", (response) => {
if (response.status() >= 400) {
popupNetworkFailures.push({
status: response.status(),
url: response.url().slice(0, 1000),
});
}
});
try {
await page.waitForFunction(
() => window.__MNOTE_ONLYOFFICE_READY__ === true,
undefined,
{ timeout: ONLYOFFICE_READY_TIMEOUT_MS },
);
await page.waitForFunction(
() => {
const root = document.getElementById("onlyoffice-frame");
if (root && root.querySelector("iframe,canvas")) return true;
return Boolean(document.querySelector("iframe,canvas"));
},
undefined,
{ timeout: ONLYOFFICE_READY_TIMEOUT_MS },
);
} catch (error) {
const debug = await page.evaluate(() => ({
url: window.location.href,
title: document.title,
readyState: document.readyState,
ready: window.__MNOTE_ONLYOFFICE_READY__ === true,
debug: window.__MNOTE_ONLYOFFICE_DEBUG__ || null,
errors: window.__MNOTE_ONLYOFFICE_ERRLOG__ || [],
hasFrameRoot: Boolean(document.getElementById("onlyoffice-frame")),
bodyText: document.body ? document.body.innerText.slice(0, 800) : "",
html: document.body ? document.body.innerHTML.slice(0, 1600) : "",
scriptCount: document.scripts.length,
frameCount: document.querySelectorAll("iframe,canvas").length,
hasFrameRoot: Boolean(document.getElementById("onlyoffice-frame")),
})).catch((debugError) => ({
evaluateError: debugError instanceof Error ? debugError.message : String(debugError),
isClosed: page.isClosed(),
}));
debug.consoleMessages = popupConsoleMessages.slice(-20);
debug.networkFailures = popupNetworkFailures.slice(-30);
throw new Error(
`OnlyOffice ready 超时:${error instanceof Error ? error.message : String(error)} ${JSON.stringify(debug)}`,
);
}
}
async function readOnlyOfficeDebug(page) {
return await page.evaluate(() => {
const debug = window.__MNOTE_ONLYOFFICE_DEBUG__ || null;
return {
debug,
docKey: debug && typeof debug.docKey === "string" ? debug.docKey : "",
ready: window.__MNOTE_ONLYOFFICE_READY__ === true,
frameCount: document.querySelectorAll("iframe,canvas").length,
};
});
}
async function main() {
const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1" });
const context = await browser.newContext();
const page = await context.newPage();
let target = null;
try {
await forceTestAccountLogin(context);
await ensureAuthenticated(page, context.request);
target = await createTempDocument(context.request, null);
const asset = await uploadProbeDocx(context.request, target);
await openDocument(page, target.workspaceId, target.documentId);
await openFilesystemView(page);
const row = await waitForAssetRow(page, asset.id);
const [popup] = await Promise.all([
page.waitForEvent("popup", { timeout: UI_TIMEOUT_MS }),
row.locator('[data-rust-action="open"]').first().click({ timeout: UI_TIMEOUT_MS }),
]);
await popup.waitForLoadState("domcontentloaded", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
const opened = new URL(popup.url());
assert(opened.pathname === "/onlyoffice", `附件应在新窗口打开 /onlyoffice,实际为:${popup.url()}`);
assert(opened.searchParams.get("assetId") === asset.id, "OnlyOffice URL 缺少正确 assetId");
assert(
opened.searchParams.get("documentId") === target.documentId,
"OnlyOffice URL 缺少正确 documentId",
);
assert(opened.searchParams.get("fileType") === "docx", "OnlyOffice URL fileType 应为 docx");
assert(opened.searchParams.get("mode") === "edit", "OnlyOffice URL mode 应为 edit");
await waitForOnlyOfficeReady(popup);
const firstDebug = await readOnlyOfficeDebug(popup);
assert(firstDebug.ready, "OnlyOffice ready flag 应为 true");
assert(firstDebug.frameCount > 0, "OnlyOffice 应创建 iframe/canvas");
assert(
/^[0-9A-Za-z_.=-]{1,128}$/.test(firstDebug.docKey),
`OnlyOffice document.key 不符合安全字符集或长度:${firstDebug.docKey}`,
);
assert(
firstDebug.docKey === asset.id || firstDebug.docKey.startsWith(`${asset.id}_`),
`OnlyOffice document.key 应绑定 assetId,实际为:${firstDebug.docKey}`,
);
const reopened = await context.newPage();
await reopened.goto(popup.url(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await waitForOnlyOfficeReady(reopened);
const secondDebug = await readOnlyOfficeDebug(reopened);
assert(
secondDebug.docKey === firstDebug.docKey,
`同一附件重复打开 document.key 应稳定:first=${firstDebug.docKey} second=${secondDebug.docKey}`,
);
await reopened.close().catch(() => undefined);
console.log(
JSON.stringify(
{
ok: true,
documentId: target.documentId,
workspaceId: target.workspaceId,
assetId: asset.id,
openedUrl: popup.url(),
docKey: firstDebug.docKey,
},
null,
2,
),
);
} finally {
if (target && target.documentId) {
await purgeDocument(context.request, target.documentId).catch((error) => {
console.warn(`清理临时页面失败:${error instanceof Error ? error.message : String(error)}`);
});
}
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});