feat: 管理后台接真实接口并加 Playwright e2e
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Playwright webServer 入口:先起临时 nixmsg,再起 Vite(代理到该进程)。
|
||||
* 注意:Playwright 会在 globalSetup 之前启动 webServer。
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoRoot = path.resolve(webRoot, "..");
|
||||
const runtimePath = path.join(webRoot, "e2e", ".runtime.json");
|
||||
const pidPath = path.join(webRoot, "e2e", ".server.pid");
|
||||
const binDir = path.join(webRoot, "e2e", ".bin");
|
||||
|
||||
function run(cwd, cmd, args, extraEnv) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, args, {
|
||||
cwd,
|
||||
shell: process.platform === "win32",
|
||||
env: { ...process.env, ...extraEnv },
|
||||
});
|
||||
let out = "";
|
||||
let err = "";
|
||||
child.stdout?.on("data", (d) => {
|
||||
out += String(d);
|
||||
});
|
||||
child.stderr?.on("data", (d) => {
|
||||
err += String(d);
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) resolve(out + err);
|
||||
else reject(new Error(`${cmd} ${args.join(" ")} failed (${code}):\n${out}\n${err}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseAdminPassword(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
const lower = line.toLowerCase();
|
||||
if (lower.startsWith("password:")) return line.slice("password:".length).trim();
|
||||
if (lower.startsWith("admin password:")) return line.slice("admin password:".length).trim();
|
||||
}
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (line.length >= 12 && !line.toLowerCase().includes("error") && !/\s/.test(line)) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function waitAddr(file, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let last = "";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const addr = (await readFile(file, "utf8")).trim();
|
||||
if (addr) return addr;
|
||||
last = "empty";
|
||||
} catch (e) {
|
||||
last = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
await delay(50);
|
||||
}
|
||||
throw new Error(`wait listen.addr timeout: ${last}`);
|
||||
}
|
||||
|
||||
async function killPid(pid) {
|
||||
if (!pid) return;
|
||||
if (process.platform === "win32") {
|
||||
await new Promise((resolve) => {
|
||||
const child = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { shell: true });
|
||||
child.on("close", () => resolve());
|
||||
child.on("error", () => resolve());
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
process.kill(pid, "SIGKILL");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(dataDir, servePid) {
|
||||
await killPid(servePid);
|
||||
if (dataDir) await rm(dataDir, { recursive: true, force: true });
|
||||
await rm(runtimePath, { force: true });
|
||||
await rm(pidPath, { force: true });
|
||||
await rm(binDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(binDir, { recursive: true });
|
||||
const bin = path.join(binDir, process.platform === "win32" ? "nixmsg.exe" : "nixmsg");
|
||||
await run(repoRoot, "go", ["build", "-o", bin, "./cmd/nixmsg"]);
|
||||
|
||||
const dataDir = await mkdtemp(path.join(tmpdir(), "nixmsg-w4-e2e-"));
|
||||
const cfgPath = path.join(dataDir, "config.yaml");
|
||||
const yaml = `listen: "127.0.0.1:0"\ndata_dir: ${JSON.stringify(dataDir.replace(/\\/g, "/"))}\n`;
|
||||
await writeFile(cfgPath, yaml, "utf8");
|
||||
|
||||
const initOut = await run(repoRoot, bin, ["admin", "init"], { NIXMSG_CONFIG: cfgPath });
|
||||
const adminPassword = parseAdminPassword(initOut);
|
||||
if (!adminPassword) {
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
throw new Error(`admin init password not found:\n${initOut}`);
|
||||
}
|
||||
|
||||
const serve = spawn(bin, ["serve"], {
|
||||
env: { ...process.env, NIXMSG_CONFIG: cfgPath },
|
||||
cwd: repoRoot,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let serveLog = "";
|
||||
serve.stdout.on("data", (d) => {
|
||||
serveLog += String(d);
|
||||
});
|
||||
serve.stderr.on("data", (d) => {
|
||||
serveLog += String(d);
|
||||
});
|
||||
|
||||
let addr;
|
||||
try {
|
||||
addr = await waitAddr(path.join(dataDir, "listen.addr"), 15_000);
|
||||
} catch (e) {
|
||||
await killPid(serve.pid);
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
throw new Error(`${e}\nserve log:\n${serveLog}`);
|
||||
}
|
||||
|
||||
const vitePort = 5179;
|
||||
const info = {
|
||||
httpBase: `http://${addr}`,
|
||||
adminPassword,
|
||||
dataDir,
|
||||
binPath: bin,
|
||||
vitePort,
|
||||
servePid: serve.pid,
|
||||
};
|
||||
await writeFile(runtimePath, JSON.stringify(info, null, 2), "utf8");
|
||||
await writeFile(pidPath, String(serve.pid ?? ""), "utf8");
|
||||
|
||||
const vite = spawn(
|
||||
process.platform === "win32" ? "pnpm.cmd" : "pnpm",
|
||||
["exec", "vite", "--host", "127.0.0.1", "--port", String(vitePort), "--strictPort"],
|
||||
{
|
||||
cwd: webRoot,
|
||||
env: { ...process.env, NIXMSG_PROXY_TARGET: info.httpBase },
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
},
|
||||
);
|
||||
|
||||
const shutdown = async () => {
|
||||
vite.kill();
|
||||
await cleanup(dataDir, serve.pid);
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGINT", () => {
|
||||
void shutdown();
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
void shutdown();
|
||||
});
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
if (serve.pid) {
|
||||
if (process.platform === "win32") {
|
||||
spawn("taskkill", ["/PID", String(serve.pid), "/T", "/F"], { shell: true, detached: true, stdio: "ignore" });
|
||||
} else {
|
||||
process.kill(serve.pid, "SIGKILL");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
vite.on("exit", async (code) => {
|
||||
await cleanup(dataDir, serve.pid);
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user