/** * # 脚本 后台浏览器点击级实测(playwright + headless chromium) * 覆盖:登录态进入各页、媒体上传、内容保存(分类+Elementor)、修订弹窗、 * 备份 UI(增量执行/配置保存)、用户 Tab。输出 PASS/FAIL 清单。 */ import { chromium } from "playwright-core"; import { readFileSync, writeFileSync, mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const CHROME = process.env.CHROME || `${process.env.HOME}/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome`; const BASE = "http://127.0.0.1:3312"; const TOKEN = readFileSync("/tmp/bt-token.txt", "utf8").trim(); const results: { name: string; ok: boolean; detail?: string }[] = []; const check = (name: string, ok: boolean, detail?: string) => { results.push({ name, ok, detail }); console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`); }; const browser = await chromium.launch({ executablePath: CHROME, headless: true, args: ["--no-sandbox"] }); const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); await ctx.addCookies([{ name: "dusun_session", value: TOKEN, domain: "127.0.0.1", path: "/" }]); const page = await ctx.newPage(); try { // 1. Dashboard 渲染 await page.goto(BASE + "/admin/", { waitUntil: "networkidle" }); check("Dashboard 渲染", await page.getByText("询盘漏斗").count() > 0); // 2. 媒体上传(AntD Upload input 直设文件) await page.goto(BASE + "/admin/media/", { waitUntil: "networkidle" }); const png = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFklEQVR4nGP8//8/AzGAiShVowoJAgCCpgQDXDeUDQAAAABJRU5ErkJggg==", "base64", ); const upDir = mkdtempSync(join(tmpdir(), "bt-")); const upFile = join(upDir, "bt-upload.png"); writeFileSync(upFile, png); const fileChooserP = page.waitForEvent("filechooser"); await page.getByRole("button", { name: /上传媒体/ }).click(); const fc = await fileChooserP; await fc.setFiles([upFile]); await page.waitForTimeout(2500); const uploaded = await page.getByText("bt-upload.png").count(); check("媒体上传", uploaded > 0, uploaded ? "" : "列表未出现新文件"); // 3. 新建内容(页面类型 + 分类标签 + 保存) await page.goto(BASE + "/admin/contents/new/", { waitUntil: "networkidle" }); await page.getByLabel("标题").first().fill("浏览器实测页面"); await page.getByLabel("正文 HTML(无 Elementor JSON 时前台按此渲染)").first().fill("

bt content

"); await page.getByRole("button", { name: /保 存|保存/ }).first().click(); await page.waitForTimeout(2000); check("新建内容保存", await page.getByText("保存成功").count() > 0); // 4. 编辑该内容:绑定分类 + 填 Elementor JSON + 再保存(产生修订) await page.goto(BASE + "/admin/contents/", { waitUntil: "networkidle" }); await page.getByPlaceholder("搜索标题 / 别名").fill("浏览器实测页面"); await page.keyboard.press("Enter"); await page.waitForTimeout(1500); await page.getByRole("link", { name: "浏览器实测页面" }).first().click(); await page.waitForTimeout(1500); // TreeSelect(分类/标签) const ts = page.locator(".ant-select:not(.ant-select-customize-input)").filter({ hasText: "选择分类与标签" }); if (await ts.count()) { await ts.first().click(); await page.waitForTimeout(800); const opt = page.locator(".ant-select-tree-treenode").filter({ hasText: /.+/ }).first(); await opt.click(); await page.keyboard.press("Escape"); } await page.getByLabel(/Elementor JSON/).first().fill('{"elements":[]}'); await page.getByRole("button", { name: /保 存|保存/ }).first().click(); await page.waitForTimeout(2000); check("编辑内容+分类+Elementor 保存", await page.getByText("保存成功").count() > 0); // 保存后跳回列表:重新搜索并进入编辑页再测修订 await page.waitForTimeout(1000); await page.getByPlaceholder("搜索标题 / 别名").first().fill("浏览器实测页面"); await page.keyboard.press("Enter"); await page.waitForTimeout(1500); await page.getByRole("link", { name: "浏览器实测页面" }).first().click(); await page.waitForTimeout(1500); // 5. 修订历史弹窗 await page.getByRole("button", { name: /修订历史/ }).first().click(); await page.waitForTimeout(1500); const revRows = await page.locator(".ant-modal .ant-table-row").count(); check("修订历史留档", revRows >= 1, `修订 ${revRows} 条(创建不快照,每次更新留档)`); await page.keyboard.press("Escape"); // 6. 备份页:列表 + 增量备份执行 await page.goto(BASE + "/admin/backups/", { waitUntil: "networkidle" }); check("备份页渲染", await page.getByText("备份管理(PostgreSQL 18 物理备份)").count() > 0); await page.getByRole("button", { name: /增量备份/ }).click(); // 备份 = pg_basebackup + tar.gz + S3 上传,实测 ~10-20s await page.waitForSelector("text=/备份完成:incr-/", { timeout: 40000 }).catch(() => {}); const incrDone = await page.getByText(/备份完成:incr-/).count() > 0; if (!incrDone) await page.waitForTimeout(3000); check("UI 增量备份执行", incrDone || (await page.locator(".ant-table-row").filter({ hasText: /incr-/ }).count()) > 2); // 7. 备份配置保存(S3 留空 → 仅保留策略) await page.getByLabel(/保留全量链数/).fill("3"); await page.getByRole("button", { name: /保存配置/ }).click(); await page.waitForTimeout(1500); check("备份配置保存", await page.getByText("备份配置已保存").count() > 0); // 8. 用户页 Tab await page.goto(BASE + "/admin/users/", { waitUntil: "networkidle" }); await page.getByRole("tab", { name: "普通用户" }).click(); await page.waitForTimeout(1500); const rows = await page.locator(".ant-table-row").count(); check("用户页普通用户 Tab", rows > 0, `${rows} 行`); } catch (e) { check("脚本异常", false, e instanceof Error ? e.message.slice(0, 300) : String(e)); } await browser.close(); const fail = results.filter((r) => !r.ok); console.log(`\n==== ${results.length - fail.length}/${results.length} PASS ====`); process.exit(0);