121 lines
6.1 KiB
TypeScript
121 lines
6.1 KiB
TypeScript
/**
|
||
* # 脚本 后台浏览器点击级实测(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);
|