Files
mnote/scripts/task517-onlyoffice-bridge-plugin-direct-smoke.js

228 lines
8.6 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const { spawn } = require("node:child_process");
const net = require("node:net");
const { chromium } = require("playwright");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_UI_TIMEOUT_MS || 45000);
function findFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, "127.0.0.1", () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
async function waitForGateway(baseUrl, timeoutMs = 120000) {
const deadline = Date.now() + timeoutMs;
let lastError = "";
while (Date.now() < deadline) {
try {
const response = await fetch(`${baseUrl}/`);
if (response.status === 200 || response.status === 303) return;
lastError = `${response.status} ${await response.text().catch(() => "")}`;
} catch (error) {
lastError = error && error.message ? error.message : String(error);
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`mnote-web 未就绪: ${lastError}`);
}
function startGateway(port) {
const env = {
...process.env,
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
};
const child = spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
cwd: "/mnt/Data1T/mnote/rust",
env,
stdio: ["ignore", "pipe", "pipe"],
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.stdout.on("data", () => {});
return {
child,
stderr: () => stderr,
};
}
async function stopGateway(gateway) {
if (!gateway || gateway.child.killed) return;
gateway.child.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 500));
if (!gateway.child.killed) gateway.child.kill("SIGKILL");
}
async function requestJson(baseUrl, path, init = {}) {
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
"content-type": "application/json",
...(init.headers || {}),
},
});
const body = response.status === 204 ? null : await response.json().catch(() => null);
return { status: response.status, body };
}
async function enqueueCommand(baseUrl, sessionId, token, action, payload) {
const response = await requestJson(baseUrl, "/api/onlyoffice/bridge/commands", {
method: "POST",
body: JSON.stringify({ sessionId, token, action, payload }),
});
assert.equal(response.status, 200, JSON.stringify(response));
assert(response.body && response.body.command && response.body.command.id, JSON.stringify(response));
return response.body.command.id;
}
async function readResult(baseUrl, sessionId, token, commandId) {
const query = new URLSearchParams({
sessionId,
token,
commandId,
timeoutMs: "5000",
});
const response = await requestJson(baseUrl, `/api/onlyoffice/bridge/results?${query.toString()}`);
assert.equal(response.status, 200, JSON.stringify(response));
assert.equal(response.body && response.body.ok, true, JSON.stringify(response));
return response.body.result;
}
async function waitForSession(baseUrl, sessionId, timeoutMs = 5000) {
const deadline = Date.now() + timeoutMs;
let lastResponse = null;
while (Date.now() < deadline) {
lastResponse = await requestJson(baseUrl, "/api/onlyoffice/bridge/sessions");
if (lastResponse.status === 200 && lastResponse.body && Array.isArray(lastResponse.body.sessions)) {
const found = lastResponse.body.sessions.find((session) => session.sessionId === sessionId);
if (found) return { response: lastResponse, session: found };
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`bridge session 未注册: ${JSON.stringify(lastResponse)}`);
}
async function main() {
const port = await findFreePort();
const baseUrl = `http://127.0.0.1:${port}`;
const gateway = startGateway(port);
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const sessionId = `mnote-oo-plugin-direct-${suffix}`;
const token = `token-${suffix}`;
const consoleErrors = [];
const networkFailures = [];
try {
await waitForGateway(baseUrl);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
page.on("console", (message) => {
if (message.type() === "error") consoleErrors.push(message.text());
});
page.on("requestfailed", (request) => {
networkFailures.push({ url: request.url(), failure: request.failure()?.errorText || "" });
});
await page.route("**/onlyoffice-server/sdkjs-plugins/v1/plugins.js", async (route) => {
await route.fulfill({
contentType: "application/javascript; charset=utf-8",
body: `
window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__ = { executeCalls: [], commandCalls: [] };
window.Asc = {
scope: {},
plugin: {
info: { editorType: "word" },
executeMethod(name, args, callback) {
window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__.executeCalls.push({ name, args });
const value = name === "GetSelectedText"
? "mock selected text"
: name === "PasteText"
? true
: name === "ConvertDocument"
? "# mock document"
: null;
callback(value);
},
callCommand(func, _close, _calc, callback) {
window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__.commandCalls.push({ scope: window.Asc.scope.mnoteBridge || {} });
callback(func());
},
init: null,
button: null
}
};
`,
});
});
const pluginUrl = new URL(`${baseUrl}/api/onlyoffice/bridge/plugin/index`);
pluginUrl.searchParams.set("sessionId", sessionId);
pluginUrl.searchParams.set("token", token);
pluginUrl.searchParams.set("apiBase", baseUrl);
pluginUrl.searchParams.set("documentId", "doc_plugin_direct");
pluginUrl.searchParams.set("assetId", "asset_plugin_direct");
pluginUrl.searchParams.set("fileType", "docx");
pluginUrl.searchParams.set("docKey", "doc_key_plugin_direct");
pluginUrl.searchParams.set("pageOrigin", baseUrl);
await page.goto(pluginUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.evaluate(() => window.Asc.plugin.init());
const { session: registeredSession } = await waitForSession(baseUrl, sessionId);
assert.equal(registeredSession.docKey, "doc_key_plugin_direct", JSON.stringify(registeredSession));
assert.equal(registeredSession.pageOrigin, baseUrl, JSON.stringify(registeredSession));
const wrongToken = await requestJson(
baseUrl,
`/api/onlyoffice/bridge/commands/next?sessionId=${encodeURIComponent(sessionId)}&token=wrong-${encodeURIComponent(token)}&timeoutMs=1`,
);
assert.equal(wrongToken.status, 401, JSON.stringify(wrongToken));
const selectionCommandId = await enqueueCommand(baseUrl, sessionId, token, "selection.get", {});
const selection = await readResult(baseUrl, sessionId, token, selectionCommandId);
assert.equal(selection.editorType, "word", JSON.stringify(selection));
assert.equal(selection.text, "mock selected text", JSON.stringify(selection));
const insertCommandId = await enqueueCommand(baseUrl, sessionId, token, "document.insert_text", { text: "hello" });
const inserted = await readResult(baseUrl, sessionId, token, insertCommandId);
assert.equal(inserted.value, true, JSON.stringify(inserted));
const mockState = await page.evaluate(() => window.__MNOTE_ONLYOFFICE_PLUGIN_MOCK__);
assert(mockState.executeCalls.some((call) => call.name === "GetSelectedText"), JSON.stringify(mockState));
assert(mockState.executeCalls.some((call) => call.name === "PasteText"), JSON.stringify(mockState));
await browser.close();
console.log(JSON.stringify({
ok: true,
task: "task517-onlyoffice-bridge-plugin-direct-smoke",
baseUrl,
sessionId,
wrongToken: { status: wrongToken.status, code: wrongToken.body && wrongToken.body.code },
registeredSession,
selection,
inserted,
executeCalls: mockState.executeCalls.map((call) => call.name),
consoleErrors,
networkFailures,
}, null, 2));
} finally {
await stopGateway(gateway);
}
}
main().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});