140 lines
4.7 KiB
JavaScript
140 lines
4.7 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}/`, {
|
||
|
|
headers: {
|
||
|
|
"x-mnote-actor-id": "mnote-e2e",
|
||
|
|
"x-mnote-actor-type": "user",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
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, devHot) {
|
||
|
|
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",
|
||
|
|
};
|
||
|
|
delete env.MNOTE_WEB_DEV_HOT_RELOAD;
|
||
|
|
if (devHot) env.MNOTE_WEB_DEV_HOT_RELOAD = "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 verifyCase(devHot) {
|
||
|
|
const port = await findFreePort();
|
||
|
|
const baseUrl = `http://127.0.0.1:${port}`;
|
||
|
|
const gateway = startGateway(port, devHot);
|
||
|
|
try {
|
||
|
|
await waitForGateway(baseUrl);
|
||
|
|
const browser = await chromium.launch({ headless: true });
|
||
|
|
const context = await browser.newContext({
|
||
|
|
extraHTTPHeaders: {
|
||
|
|
"x-mnote-actor-id": "mnote-e2e",
|
||
|
|
"x-mnote-actor-type": "user",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const page = await context.newPage();
|
||
|
|
const hotReloadRequests = [];
|
||
|
|
const networkFailures = [];
|
||
|
|
const consoleErrors = [];
|
||
|
|
page.on("request", (request) => {
|
||
|
|
if (request.url().includes("/api/dev/hot-reload")) hotReloadRequests.push(request.url());
|
||
|
|
});
|
||
|
|
page.on("requestfailed", (request) => {
|
||
|
|
networkFailures.push({ url: request.url(), failure: request.failure()?.errorText || "" });
|
||
|
|
});
|
||
|
|
page.on("console", (message) => {
|
||
|
|
if (message.type() === "error") consoleErrors.push(message.text());
|
||
|
|
});
|
||
|
|
await page.goto(`${baseUrl}/`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||
|
|
await page.locator('body[data-mnote-shell="workspace"]').waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
|
||
|
|
await page.waitForTimeout(1800);
|
||
|
|
const html = await page.content();
|
||
|
|
const shell = await page.locator("body").getAttribute("data-mnote-shell");
|
||
|
|
const devHotAttr = await page.locator("html").getAttribute("data-mnote-dev-hot-reload").catch(() => null);
|
||
|
|
await browser.close();
|
||
|
|
return {
|
||
|
|
devHot,
|
||
|
|
baseUrl,
|
||
|
|
shell,
|
||
|
|
scriptHasDevHot: html.includes("sidebar-tree-runtime.js?devHot="),
|
||
|
|
hotReloadRequestCount: hotReloadRequests.length,
|
||
|
|
hotReloadAttr: devHotAttr || "",
|
||
|
|
networkFailures,
|
||
|
|
consoleErrors,
|
||
|
|
};
|
||
|
|
} finally {
|
||
|
|
await stopGateway(gateway);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const normal = await verifyCase(false);
|
||
|
|
const hot = await verifyCase(true);
|
||
|
|
assert.equal(normal.shell, "workspace", JSON.stringify(normal));
|
||
|
|
assert.equal(normal.scriptHasDevHot, false, JSON.stringify(normal));
|
||
|
|
assert.equal(normal.hotReloadRequestCount, 0, JSON.stringify(normal));
|
||
|
|
assert.equal(hot.shell, "workspace", JSON.stringify(hot));
|
||
|
|
assert.equal(hot.scriptHasDevHot, true, JSON.stringify(hot));
|
||
|
|
assert(hot.hotReloadRequestCount >= 1, JSON.stringify(hot));
|
||
|
|
assert.equal(hot.hotReloadAttr, "enabled", JSON.stringify(hot));
|
||
|
|
console.log(JSON.stringify({ ok: true, task: "task514-sidebar-dev-hot-reload-gating-smoke", normal, hot }, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((error) => {
|
||
|
|
console.error(error && error.stack ? error.stack : error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|