feat: v2.3.0 设置内配置 Cloudflare OAuth 客户端 + 全列表批量删除
- 设置新增「Cloudflare 接入」tab:Client ID / Client Secret(Secret AES-GCM 加密入库,
只回显末 4 位),并展示可复制的授权回调地址;settings 优先、env 变量兜底,
保存后立即生效无需重新部署;DELETE /api/settings/cf-oauth 清除
- 四个资源列表(账号/域名/邮箱/收件箱)加复选框批量选择与批量删除
- 新增 POST /api/{accounts,domains,mailboxes,emails}/batch-delete:
账号软删+撤销凭据;域名/邮箱级联删除其下邮件;邮件连 R2 原文与附件一并清理
- 抽出 deleteEmailById / deleteEmailsByMailbox / revokeAccount 供单条与批量共用
- 点击复选框不再触发行点击(避免误开详情抽屉)
- 新增 tests/batch.test.ts(6 例)与 settings OAuth 凭据用例(2 例);ui-smoke 扩到 17 步
This commit is contained in:
+38
-2
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* UI 冒烟测试(Playwright headless):
|
||||
* 登录 admin/admin → 概览/账号/域名/邮箱/邮件 各页渲染 → 设置模态窗口 → 生成 Key → 截图。
|
||||
* 登录 admin/admin → 概览/账号/域名/邮箱/邮件 各页渲染 → 列表复选框批量选择 →
|
||||
* 设置模态窗口(含 Cloudflare 接入 tab)→ 生成 Key → 截图。
|
||||
* bun tests/ui-smoke.ts
|
||||
*/
|
||||
|
||||
@@ -91,9 +92,25 @@ await page.locator(".ant-dropdown-trigger, .app-header .ant-dropdown-trigger").f
|
||||
await page.waitForSelector(".ant-dropdown:not(.ant-dropdown-hidden)", { timeout: 8000 });
|
||||
await page.click(".ant-dropdown .ant-dropdown-menu-item:has-text('设置')");
|
||||
await page.waitForSelector(".ant-modal:has-text('全局 Webhook')", { timeout: 8000 });
|
||||
check("设置模态窗口经头像下拉打开(仅 全局 Webhook / 账号密码 两 tab)", true);
|
||||
const tabLabels = await page.locator(".ant-modal .ant-tabs-tab").allInnerTexts();
|
||||
check(
|
||||
"设置模态窗口经头像下拉打开(全局 Webhook / Cloudflare 接入 / 账号密码 三 tab)",
|
||||
tabLabels.length === 3 && tabLabels.some((t) => t.includes("Cloudflare 接入")),
|
||||
tabLabels.join(" | "),
|
||||
);
|
||||
await page.screenshot({ path: `${SHOT_DIR}/09-settings-webhook.png` });
|
||||
|
||||
// Cloudflare 接入 tab:回调地址可复制 + Client ID/Secret 表单
|
||||
await page.click(".ant-modal .ant-tabs-tab:has-text('Cloudflare 接入')");
|
||||
await page.waitForSelector(".ant-modal :text('Client ID')", { timeout: 8000 });
|
||||
// 回调地址是 Typography.Paragraph(className=mono);同模态内还有 API Key 的 ****last4,按文本过滤
|
||||
const redirectText =
|
||||
(await page.locator(".ant-modal .mono").filter({ hasText: "/api/oauth/cloudflare/callback" }).first().textContent()) ?? "";
|
||||
check("Cloudflare 接入 tab 显示授权回调地址", redirectText.includes("/api/oauth/cloudflare/callback"), redirectText.trim());
|
||||
await page.screenshot({ path: `${SHOT_DIR}/09b-settings-cloudflare.png` });
|
||||
await page.click(".ant-modal .ant-tabs-tab:has-text('全局 Webhook')");
|
||||
await page.waitForSelector(".ant-modal :text('Webhook URL')", { timeout: 8000 });
|
||||
|
||||
// 单一 API/MCP Key:在 全局 Webhook tab 内生成
|
||||
const genBtn = page.getByRole("button", { name: /生成/ });
|
||||
await genBtn.waitFor({ state: "visible", timeout: 8000 });
|
||||
@@ -107,6 +124,25 @@ await page.screenshot({ path: `${SHOT_DIR}/10-api-key-created.png` });
|
||||
const themeBtn = await page.locator("button[aria-label='切换主题']").count();
|
||||
check("主题切换按钮存在(头像左侧)", themeBtn === 1);
|
||||
|
||||
// 列表复选框批量选择:四个列表页勾选表头全选后出现批量操作条
|
||||
for (const path of ["/accounts", "/domains", "/mailboxes", "/inboxs"]) {
|
||||
await page.goto(`${BASE}${path}`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(".ant-table-thead .ant-checkbox-input", { timeout: 8000 });
|
||||
const boxes = page.locator(".ant-table-thead .ant-checkbox-input");
|
||||
if ((await boxes.count()) === 0) {
|
||||
check(`${path} 复选框批量选择`, false, "无表头复选框");
|
||||
continue;
|
||||
}
|
||||
await boxes.first().click({ force: true });
|
||||
const barVisible = await page
|
||||
.waitForSelector("button:has-text('批量删除')", { timeout: 6000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
const selectedText = (await page.locator("text=/已选 \\d+ 项/").first().textContent().catch(() => "")) ?? "";
|
||||
check(`${path} 复选框批量选择`, barVisible && /已选/.test(selectedText), selectedText.trim());
|
||||
if (path === "/mailboxes") await page.screenshot({ path: `${SHOT_DIR}/11-batch-selected.png` });
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
|
||||
Reference in New Issue
Block a user