- 两阶段接收:POST /internal/email/authorize(accept/reject/temporary_error)+ POST /internal/email/ingest(raw MIME 直传) - Worker-Console 协议:Authorization: WmailHub-Worker + timestamp/nonce/body-hash/HMAC,nonce 防重放,凭据撤销即时生效 - 数据模型:nodes/node_credentials -> cloudflare_accounts/worker_credentials(迁移 0003),emails/domains 归属 account_id - 接入流程 Connect Cloudflare Account:OAuth 自动同步域名 + 部署账号级 Email Worker + 签发 Worker 凭据;rotate 自动重部署 - 动态邮箱:创建/删除/禁用下一封邮件立即生效;拒绝码 DOMAIN_*/MAILBOX_*;超时/5xx 不伪装成邮箱不存在 - 删除 node/ 目录、旧 /api/internal/nodes 协议、ingress/enrollment 概念;dev.ts 简化为 console + email worker 双进程 - 前端:账号页 Email Worker/凭据管理,设置窗口移除默认 Ingress,仪表盘/文案同步;版本 2.0.0 - 测试 62 项全过(新增 authorize 决策矩阵 + Worker 两阶段临时错误语义);本机虚拟 E2E 12 场景全通 - docs 00-10 同步改写;README 更新
111 lines
5.3 KiB
TypeScript
111 lines
5.3 KiB
TypeScript
/**
|
|
* UI 冒烟测试(Playwright headless):
|
|
* 登录 admin/admin → 概览/账号/域名/邮箱/邮件 各页渲染 → 设置模态窗口 → 生成 Key → 截图。
|
|
* bun tests/ui-smoke.ts
|
|
*/
|
|
|
|
import { chromium } from "playwright-core";
|
|
|
|
const BASE = process.env.WMAILHUI_BASE ?? "http://127.0.0.1:18787";
|
|
const EXEC = `${process.env.HOME}/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome`;
|
|
const SHOT_DIR = "/tmp/wmh-shots";
|
|
await Bun.$`mkdir -p ${SHOT_DIR}`.quiet();
|
|
|
|
const browser = await chromium.launch({
|
|
executablePath: EXEC,
|
|
headless: true,
|
|
args: ["--no-sandbox"],
|
|
});
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (err) => errors.push(`pageerror: ${err.message}`));
|
|
page.on("console", (msg) => {
|
|
if (msg.type() === "error") errors.push(`console: ${msg.text()}`);
|
|
});
|
|
|
|
const results: { step: string; ok: boolean; note?: string }[] = [];
|
|
const check = (step: string, ok: boolean, note?: string) => {
|
|
results.push({ step, ok, note });
|
|
console.log(`${ok ? "✓" : "✗"} ${step}${note ? ` — ${note}` : ""}`);
|
|
};
|
|
|
|
// 1. 登录页
|
|
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForSelector("input[placeholder='用户名']", { timeout: 8000 });
|
|
check("登录页渲染(含 LOGO/表单)", (await page.title()).includes("WmailHub"));
|
|
await page.screenshot({ path: `${SHOT_DIR}/01-login.png` });
|
|
|
|
// 2. 登录 admin/admin
|
|
await page.fill("input[placeholder='用户名']", "admin");
|
|
await page.fill("input[placeholder='密码']", "admin");
|
|
await page.click("button[type=submit]");
|
|
await page.waitForURL(`${BASE}/`, { timeout: 8000 });
|
|
await page.waitForSelector("text=概览", { timeout: 8000 });
|
|
check("登录成功进入概览页", true);
|
|
// 品牌区版本号 tag(v1.1.0,来源 package.json)
|
|
const versionTag = await page.locator(".brand-version").textContent();
|
|
check("LOGO 版本号 tag 显示", Boolean(versionTag && /^v\d+\.\d+\.\d+$/.test(versionTag.trim())), versionTag?.trim());
|
|
await page.waitForTimeout(600);
|
|
await page.screenshot({ path: `${SHOT_DIR}/02-dashboard.png` });
|
|
|
|
// 3. 各页面导航渲染
|
|
const pages = [
|
|
{ path: "/accounts", expect: "账号", shot: "03-accounts.png" },
|
|
{ path: "/domains", expect: "域名", shot: "04-domains.png" },
|
|
{ path: "/mailboxes", expect: "邮箱", shot: "05-mailboxes.png" },
|
|
{ path: "/emails", expect: "邮件", shot: "06-emails.png" },
|
|
];
|
|
for (const p of pages) {
|
|
await page.goto(`${BASE}${p.path}`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForSelector(`text=${p.expect}`, { timeout: 8000 });
|
|
await page.waitForTimeout(500);
|
|
const hasTable = (await page.locator(".ant-table").count()) > 0;
|
|
const hasSearch = (await page.locator("input[placeholder*='搜索']").count()) > 0;
|
|
check(`页面 ${p.path} 渲染${hasTable ? "(含表格)" : ""}${hasSearch ? "(含搜索框)" : ""}`, hasTable);
|
|
await page.screenshot({ path: `${SHOT_DIR}/${p.shot}` });
|
|
}
|
|
|
|
// 4. 域名详情抽屉(点击第一行:Email Workers + Webhook 配置)
|
|
await page.goto(`${BASE}/domains`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForSelector(".ant-table-row", { timeout: 8000 });
|
|
await page.locator(".ant-table-row").first().click();
|
|
await page.waitForSelector(".ant-drawer-open", { timeout: 8000 });
|
|
await page.waitForSelector("text=域名级 Webhook", { timeout: 8000 });
|
|
check("域名详情抽屉(Email Workers + Webhook 配置)", true);
|
|
await page.screenshot({ path: `${SHOT_DIR}/07-domain-drawer.png` });
|
|
await page.keyboard.press("Escape");
|
|
|
|
// 5. 邮件详情(点击第一行)
|
|
await page.goto(`${BASE}/emails`, { waitUntil: "domcontentloaded" });
|
|
await page.waitForSelector(".ant-table-row", { timeout: 8000 });
|
|
await page.locator(".ant-table-row").first().click();
|
|
await page.waitForSelector(".ant-drawer-open", { timeout: 8000 });
|
|
const drawerText = (await page.locator(".ant-drawer-title").textContent()) ?? "";
|
|
check("邮件详情抽屉打开(标题=邮件主题)", drawerText.trim().length > 0, drawerText.trim());
|
|
await page.screenshot({ path: `${SHOT_DIR}/08-email-detail.png` });
|
|
await page.keyboard.press("Escape");
|
|
|
|
// 6. 设置模态窗口:基本设置 + 生成 MCP Key
|
|
await page.locator("button[aria-label='设置']").click();
|
|
await page.waitForSelector(".ant-modal:has-text('全局 Webhook')", { timeout: 8000 });
|
|
check("设置模态窗口打开(全局 Webhook / Ingress / 密码)", true);
|
|
await page.screenshot({ path: `${SHOT_DIR}/09-settings-general.png` });
|
|
|
|
await page.click(".ant-tabs-tab:has-text('MCP Keys')");
|
|
await page.waitForSelector("button:has-text('生成 MCP Key')", { timeout: 8000 });
|
|
await page.click("button:has-text('生成 MCP Key')");
|
|
await page.waitForSelector(".ant-modal:has-text('仅显示一次')", { timeout: 8000 });
|
|
const keyText = await page.locator(".ant-modal:has-text('仅显示一次') .mono").first().textContent();
|
|
check("生成 MCP Key 弹窗(wmh_ 32 位)", Boolean(keyText && keyText.trim().startsWith("wmh_") && keyText.trim().length === 32), keyText?.trim());
|
|
await page.screenshot({ path: `${SHOT_DIR}/10-mcp-key-created.png` });
|
|
|
|
await browser.close();
|
|
|
|
const failed = results.filter((r) => !r.ok);
|
|
console.log(`\n==== UI 冒烟结果: ${results.length - failed.length}/${results.length} 通过 ====`);
|
|
if (errors.length > 0) {
|
|
console.log("浏览器错误:");
|
|
for (const e of errors.slice(0, 10)) console.log(" ", e);
|
|
}
|
|
process.exit(failed.length > 0 ? 1 : 0);
|