#!/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); });