228 lines
8.6 KiB
JavaScript
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);
|
|
});
|