Files
dusuniot/scripts/admin-browser-test.ts

121 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 后台浏览器点击级实测(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("<p>bt content</p>");
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);