import { expect, test, type APIRequestContext, type Page } from "@playwright/test"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; type RuntimeInfo = { httpBase: string; adminPassword: string; dataDir: string; binPath: string; vitePort: number; }; const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = path.resolve(webRoot, ".."); const runtimePath = path.join(webRoot, "e2e", ".runtime.json"); async function loadRuntime(): Promise { return JSON.parse(await readFile(runtimePath, "utf8")) as RuntimeInfo; } async function login(page: Page, password: string) { await page.goto("/login"); await page.getByPlaceholder("admin").fill("admin"); await page.getByPlaceholder("请输入密码").fill(password); await page.getByTestId("login-submit").click(); await expect(page).toHaveURL(/\/overview/); } function nav(page: Page, label: string) { return page.locator(".n-menu").getByText(label, { exact: true }).click(); } async function createPeerViaAdmin( request: APIRequestContext, httpBase: string, password: string, id: string, loginPassword: string, ) { const loginRes = await request.post(`${httpBase}/api/admin/login`, { data: { username: "admin", password }, }); expect(loginRes.ok()).toBeTruthy(); const res = await request.post(`${httpBase}/api/admin/endpoints`, { headers: { "X-Nixmsg-Request": "1" }, data: { id, name: id, login_password: loginPassword }, }); const body = await res.json(); expect(res.ok(), JSON.stringify(body)).toBeTruthy(); expect(body.ok).toBe(true); } function seedPending(httpBase: string, from: string, fromPass: string, to: string): Promise { return new Promise((resolve, reject) => { const child = spawn( "go", [ "run", "./web/e2e/seedpending", "-base", httpBase, "-from", from, "-from-pass", fromPass, "-to", to, ], { cwd: repoRoot, shell: process.platform === "win32" }, ); let out = ""; child.stdout.on("data", (d) => { out += String(d); }); child.stderr.on("data", (d) => { out += String(d); }); child.on("error", reject); child.on("close", (code) => { if (code === 0) resolve(); else reject(new Error(`seedpending failed: ${out}`)); }); }); } test.describe("后台主路径 W4", () => { test("登录、开通、注册、建群、未完成投递、令牌创建停用", async ({ page, request }) => { const rt = await loadRuntime(); await login(page, rt.adminPassword); await nav(page, "端"); await expect(page).toHaveURL(/\/endpoints/); await page.getByRole("button", { name: "开通", exact: true }).click(); await page.getByTestId("create-id").locator("input").fill("w4ep001"); await page.getByTestId("create-name").locator("input").fill("W4端一"); await page.getByTestId("create-submit").click(); await expect(page.getByText("只显示一次")).toBeVisible(); await page.getByRole("button", { name: "我已保存" }).click(); await expect(page.getByText("只显示一次")).toHaveCount(0); await expect(page.getByText("w4ep001")).toBeVisible(); // 管理接口开通第二端(已知密码),供 MQTT 造未完成投递;管理 messages 只读 await createPeerViaAdmin(request, rt.httpBase, rt.adminPassword, "w4ep002", "password12ab"); await nav(page, "注册设置"); await expect(page).toHaveURL(/\/registration/); await page.getByTestId("reg-enabled").click(); await expect(page.getByText("已开启自助注册")).toBeVisible(); await page.getByTestId("reg-code").locator("input").fill("w4-reg-code-01"); await page.getByTestId("reg-save-code").click(); await expect(page.getByText("安全码已更新")).toBeVisible(); await nav(page, "群"); await expect(page).toHaveURL(/\/groups/); await page.getByTestId("group-create-open").click(); await page.getByTestId("group-name").locator("input").fill("W4测试群"); await page.getByTestId("group-owner").locator("input").fill("w4ep001"); await page.getByTestId("group-members").locator("textarea").fill("w4ep002"); await page.getByTestId("group-create-submit").click(); await expect(page.getByText("W4测试群")).toBeVisible(); await seedPending(rt.httpBase, "w4ep002", "password12ab", "w4ep001"); await nav(page, "投递记录"); await expect(page).toHaveURL(/\/messages/); await expect(page.getByText("w4-e2e-scheduled")).toBeVisible(); await expect(page.getByRole("cell", { name: "scheduled", exact: true })).toBeVisible(); await nav(page, "API 令牌"); await expect(page).toHaveURL(/\/tokens/); await page.getByTestId("token-create-open").click(); await page.getByTestId("token-name").locator("input").fill("w4-e2e-token"); await page.getByTestId("token-create-submit").click(); await expect(page.getByText("令牌只显示一次")).toBeVisible(); const bodyText = await page.locator("body").innerText(); const tokenMatch = bodyText.match(/nxm_[A-Za-z0-9_-]+/); expect(tokenMatch).toBeTruthy(); const shown = tokenMatch![0]; await page.getByRole("button", { name: "我已保存" }).click(); await expect(page.getByText("令牌只显示一次")).toHaveCount(0); await expect(page.getByText(shown)).toHaveCount(0); await expect(page.getByText("w4-e2e-token")).toBeVisible(); const row = page.locator("tr", { hasText: "w4-e2e-token" }); await expect(row.getByText("是")).toBeVisible(); await row.getByRole("button", { name: "停用" }).click(); await expect(row.getByText("否")).toBeVisible(); }); });