v2.4.0 修 Email Routing 假成功 + 加 API Token 接入与邮箱/域名页交互改进
问题 6(收不到邮件、Dashboard 邮件路由里看不到域名)的根因:
上一版把「开启 Email Routing」改成了 PUT /zones/{id}/email/routing,
但实测该接口(以及 PATCH)是**纯回显** —— 返回 success:true 但 enabled 纹丝不动,
域名在 Cloudflare 侧仍是 unconfigured、连 MX 记录都没有。Console 却照它的返回值
把域名标成「已配置 Email Workers」,于是「显示配好了却收不到邮件」。
实测只有 POST /zones/{id}/email/routing/enable 真的开启(线上对 adx9.com 与
galaxyemail.com 逐一验证)。
后端
- cfapi: 改用 /enable,并在开启后回读 GET 校验 enabled===true,不满足即判失败;
新增 CFApiError(带 Cloudflare 错误码)与 getEmailRoutingSettings
- cfapi: 错误码 2008(Non-Cloudflare MX records exist)翻译成中文并点名冲突的 MX 记录
- provision: Email Worker 统一命名 wmailhub-email(原先按账号 id 拼名),
部署走 PUT,同名即覆盖更新
- db: 新增 decryptAccountToken —— 按账号取 token,不再取「最近一条连接」,
并跳过已过期的 OAuth token;oauth_connections.account_id 真正写入
- resources: 新增 POST /api/accounts/token(API Token 方式添加账号):
先校验 token 与账户 ID,确认可用才建账号并自动接入
前端
- 账号:「添加账号」改为模态窗口,OAuth / API Token 两个 tab,各自带说明
- 域名: 删除「Email Worker」列;「设置 Email Workers / 重新配置」先弹确认框
(说明会改 MX 与路由),执行期间按钮 loading 且互相禁用;error 状态改成可读的 Alert + 重试
- 邮箱: 删除「状态」列(已由「启用」开关承担);地址列加复制按钮(带弹跳+变绿动画);
「添加邮箱」窗口的域名只列已装 Email Workers 的、支持搜索、默认选中最近使用的域名;
地址框可一键从 namedb 取随机用户名;勾选 catch-all 后地址框禁用
- 收件箱: 域名筛选只显示已装 Email Workers 的域名
测试
- cf-mock 复刻「PUT/PATCH 回 success 但不改 enabled」与 2008,锁住本次修复
- oauth.test 新增 4 例:2008 冲突、回读校验、API Token 接入、Token/账户 ID 校验失败
- ui-smoke 覆盖新增的添加账号/添加邮箱/确认框等交互(28/28 通过)
文档: docs/02 补「三种开启接口只有 /enable 有效」的实测表、2008 处理、
两种接入方式与按账号取 token 的说明
This commit is contained in:
+39
-8
@@ -33,6 +33,10 @@ export interface CfMockOptions {
|
||||
scopesAllowed?: string[];
|
||||
/** 授权端点固定错误(走 Cloudflare 自己的 /oauth/error 页,不弹回回调地址) **/
|
||||
authorizeError?: string;
|
||||
/** Email Routing 开启被拒(模拟 2008:域名上已有非 Cloudflare 的 MX 记录) **/
|
||||
emailRoutingBlocked?: boolean;
|
||||
/** emailRoutingBlocked 时 /dns_records 返回的 MX 列表(用于验证中文提示点名了冲突记录) **/
|
||||
foreignMx?: { id?: string; name: string; content: string; priority?: number }[];
|
||||
}
|
||||
|
||||
/** # 成员变量: Cloudflare API 桩句柄 **/
|
||||
@@ -66,6 +70,8 @@ export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
|
||||
const accessToken = opts.accessToken ?? "cf-oauth-access-token-xyz";
|
||||
const calls: CfCall[] = [];
|
||||
const original = globalThis.fetch;
|
||||
// Email Routing 的真实开关状态:PUT/PATCH 不会改它,只有 POST .../enable 会
|
||||
const state = { emailRoutingEnabled: false };
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
@@ -96,6 +102,13 @@ export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
/** Cloudflare 风格的失败信封(success:false + errors 数组) */
|
||||
const jsonError = (errors: { code: number; message: string }[], status: number) =>
|
||||
new Response(JSON.stringify({ success: false, errors, messages: [], result: null }), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (url.startsWith(CF_AUTHORIZE_URL)) {
|
||||
if (opts.authorizeError) {
|
||||
return new Response(null, {
|
||||
@@ -147,18 +160,36 @@ export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
|
||||
}
|
||||
if (url.startsWith(`${API_BASE}/zones`)) {
|
||||
if (url.includes("/email/routing/rules/catch_all")) return json({ id: "catch-all-rule" });
|
||||
if (url.endsWith("/email/routing")) {
|
||||
// 开启 Email Routing 的正确方法是 PUT;POST 到同一路径不是有效路由,
|
||||
// 真实 Cloudflare 返回 10405(2026-09-30 线上踩过),这里照抄以便锁住修复。
|
||||
if (method !== "PUT") {
|
||||
return json(
|
||||
{ success: false, errors: [{ code: 10405, message: "Method not allowed for this authentication scheme" }], result: null },
|
||||
// 开启 Email Routing 只有一个接口真的干活,这里照抄 Cloudflare 的真实行为:
|
||||
// POST .../email/routing/enable -> 真的把 enabled 翻成 true
|
||||
// PUT / PATCH .../email/routing -> 回 success:true 但 enabled 原封不动(纯回显)
|
||||
// 2026-09-30 线上正是因为信了 PUT 的返回值,把没开成 Email Routing 的域名标成了「已配置」,
|
||||
// 表现为域名显示配好了却收不到邮件、Dashboard 邮件路由里也找不到它。
|
||||
if (url.endsWith("/email/routing/enable")) {
|
||||
if (method !== "POST") {
|
||||
return jsonError(
|
||||
[{ code: 10405, message: "Method not allowed for this authentication scheme" }],
|
||||
405,
|
||||
);
|
||||
}
|
||||
return json({ id: url.split("/").at(-2), enabled: true, skip_wizard: true, status: "ready" });
|
||||
if (opts.emailRoutingBlocked) {
|
||||
return jsonError([{ code: 2008, message: "Non-Cloudflare MX records exist" }], 400);
|
||||
}
|
||||
state.emailRoutingEnabled = true;
|
||||
return json({ id: url.split("/").at(-3), enabled: true, skip_wizard: true, status: "ready" });
|
||||
}
|
||||
if (url.includes("/email/routing")) return json({ enabled: true });
|
||||
if (url.endsWith("/email/routing")) {
|
||||
// 回显型接口:无论 PUT 还是 PATCH,一律 success 但绝不改 enabled
|
||||
return json({
|
||||
id: url.split("/").at(-2),
|
||||
name: zones[0]?.name ?? "example.com",
|
||||
enabled: state.emailRoutingEnabled,
|
||||
skip_wizard: true,
|
||||
status: state.emailRoutingEnabled ? "ready" : "unconfigured",
|
||||
});
|
||||
}
|
||||
// 开启被 2008 挡下时,补一次 MX 查询,验证中文提示里确实点名了冲突记录
|
||||
if (url.includes("/dns_records")) return json(opts.foreignMx ?? []);
|
||||
return json(zones);
|
||||
}
|
||||
if (url.includes("/workers/scripts/")) return json({ id: url.split("/").pop() });
|
||||
|
||||
+166
-6
@@ -16,12 +16,14 @@ afterEach(() => {
|
||||
|
||||
/** # 函数
|
||||
* 走完 start -> callback,返回账号 id 与桩(不携带会话 cookie 调 callback,模拟跨站重定向)
|
||||
* 参数: opts - 传给 Cloudflare 桩的配置(如模拟 Email Routing 开启失败)
|
||||
*/
|
||||
async function connectAccount(
|
||||
t: Awaited<ReturnType<typeof makeTestEnv>>,
|
||||
cookie: string,
|
||||
opts: Parameters<typeof installCloudflareMock>[0] = {},
|
||||
): Promise<{ accountId: string; cf: CfMock; resp: Response }> {
|
||||
const cf = installCloudflareMock();
|
||||
const cf = installCloudflareMock(opts);
|
||||
mock = cf;
|
||||
const start = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start")
|
||||
@@ -63,7 +65,7 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
for (const key of ["oauth", "discover", "domains", "worker"]) {
|
||||
expect(detail.steps[key].status).toBe("done");
|
||||
}
|
||||
expect(detail.account.worker_id).toContain("wmailhub-email-"); // Worker credential 已签发
|
||||
expect(detail.account.worker_id).toBe("wmailhub-email"); // 统一 Worker 名(已存在则覆盖更新)
|
||||
expect(detail.credential).not.toBeNull();
|
||||
expect(detail.credential!.worker_id).toBe(detail.account.worker_id!);
|
||||
const credRow = await t.env.DB.prepare(
|
||||
@@ -111,17 +113,28 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
await authedFetch(t, cookie, `http://localhost/api/domains/${example.id}/setup`, { method: "POST" })
|
||||
).json()) as { ok: boolean; error?: string };
|
||||
expect(setup.ok).toBe(true);
|
||||
// 开启 Email Routing 必须是 PUT(POST 到同一路径是 10405,桩会拒)
|
||||
expect(cf.hit("PUT", "/zones/zone-example/email/routing")).toBe(true);
|
||||
// 开启 Email Routing 必须走 POST .../email/routing/enable。
|
||||
// PUT/PATCH /email/routing 会回 success 但 enabled 不动(桩已复刻这个行为),
|
||||
// 照它的返回值判断就会把没开成 Email Routing 的域名标成「已配置」——
|
||||
// 这正是 2026-09-30 线上「域名显示配好了却收不到邮件」的根因。
|
||||
expect(cf.hit("POST", "/zones/zone-example/email/routing/enable")).toBe(true);
|
||||
expect(cf.calls.some((c) => c.method === "POST" && c.path === "/zones/zone-example/email/routing")).toBe(false);
|
||||
expect(cf.bodyOf("PUT", "/zones/zone-example/email/routing")).toContain('"enabled":true');
|
||||
expect(
|
||||
cf.calls.some(
|
||||
(c) =>
|
||||
(c.method === "PUT" || c.method === "PATCH") &&
|
||||
c.path === "/zones/zone-example/email/routing",
|
||||
),
|
||||
).toBe(false);
|
||||
// 开启后回读状态做校验:接口说成功不算数,enabled 为 true 才算开成
|
||||
expect(cf.hit("GET", "/zones/zone-example/email/routing")).toBe(true);
|
||||
expect(cf.hit("PUT", "/zones/zone-example/email/routing/rules/catch_all")).toBe(true);
|
||||
const after = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; name: string; status: string; worker_id: string | null }[] };
|
||||
const exampleAfter = after.items.find((d) => d.id === example.id)!;
|
||||
expect(exampleAfter.status).toBe("active");
|
||||
expect(exampleAfter.worker_id).toContain("wmailhub-email-");
|
||||
expect(exampleAfter.worker_id).toBe("wmailhub-email");
|
||||
|
||||
// OAuth token 已加密入库(不含明文 token)
|
||||
const conn = await t.env.DB.prepare("SELECT encrypted_access_token FROM oauth_connections").first<{
|
||||
@@ -537,3 +550,150 @@ describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
expect(resp.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Email Routing 开启失败与回读校验(2026-09-30 线上 bug 的回归锁)", () => {
|
||||
test("域名上已有非 Cloudflare MX(2008):标 error 并点名冲突记录,不谎报已配置", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
const { accountId, cf } = await connectAccount(t, cookie, {
|
||||
emailRoutingBlocked: true,
|
||||
foreignMx: [{ name: "example.com", content: "mx.example.com", priority: 10 }],
|
||||
});
|
||||
|
||||
const domList = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; name: string; status: string }[] };
|
||||
const example = domList.items.find((d) => d.name === "example.com")!;
|
||||
const setup = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/domains/${example.id}/setup`, { method: "POST" })
|
||||
).json()) as { ok: boolean; error?: string };
|
||||
|
||||
// 关键:开不成就不能标 active —— 这正是线上"显示已配置却收不到邮件"的成因
|
||||
expect(setup.ok).toBe(false);
|
||||
const after = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; status: string; setup_error: string | null }[] };
|
||||
const row = after.items.find((d) => d.id === example.id)!;
|
||||
expect(row.status).toBe("error");
|
||||
// 错误信息要能指导用户行动:说清是 MX 冲突,并点名是哪条记录
|
||||
expect(row.setup_error).toContain("非 Cloudflare 的 MX 记录");
|
||||
expect(row.setup_error).toContain("mx.example.com");
|
||||
// catch-all 规则不该在 Routing 没开成的情况下被改写
|
||||
expect(cf.hit("PUT", "/zones/zone-example/email/routing/rules/catch_all")).toBe(false);
|
||||
// 账号仍要有 Worker 凭据(脚本已部署,只是 Routing 没开成)
|
||||
const detail = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/accounts/${accountId}`)
|
||||
).json()) as { credential: unknown };
|
||||
expect(detail.credential).not.toBeNull();
|
||||
});
|
||||
|
||||
test("开启成功但回读 enabled=false:判失败而不是标记已配置", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
await connectAccount(t, cookie);
|
||||
const domList = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; name: string }[] };
|
||||
const example = domList.items.find((d) => d.name === "example.com")!;
|
||||
|
||||
// 让桩的 /enable 变成"回 success 但不改状态":直接把 fetch 换成半吊子实现
|
||||
const base = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.endsWith("/email/routing/enable")) {
|
||||
return new Response(JSON.stringify({ success: true, errors: [], result: null }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return base(input as RequestInfo, init);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
const setup = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/domains/${example.id}/setup`, { method: "POST" })
|
||||
).json()) as { ok: boolean; error?: string };
|
||||
expect(setup.ok).toBe(false);
|
||||
expect(setup.error).toContain("enabled=false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Token 方式添加账号(与 OAuth 并列的第二种接入方式)", () => {
|
||||
test("填 Token 即可接入:token 加密入库并绑定到该账号,域名同步完成", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
const cf = installCloudflareMock({ accessToken: "cf-user-api-token-abc" });
|
||||
mock = cf;
|
||||
|
||||
const resp = await authedFetch(t, cookie, "http://localhost/api/accounts/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: "我的主账号",
|
||||
cloudflare_account_id: "cf-acct-real-1",
|
||||
token: "cf-user-api-token-abc",
|
||||
}),
|
||||
});
|
||||
const body = (await resp.json()) as { ok: boolean; account_id: string; error?: string };
|
||||
expect(resp.status).toBe(200);
|
||||
expect(body.ok).toBe(true);
|
||||
|
||||
// 账号建好且按 Cloudflare 实际账户命名
|
||||
const detail = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/accounts/${body.account_id}`)
|
||||
).json()) as {
|
||||
account: { name: string; cloudflare_account_id: string; worker_id: string; status: string };
|
||||
domains: { name: string; status: string }[];
|
||||
};
|
||||
expect(detail.account.status).toBe("active");
|
||||
expect(detail.account.cloudflare_account_id).toBe("cf-acct-real-1");
|
||||
expect(detail.account.worker_id).toBe("wmailhub-email");
|
||||
expect(detail.domains.map((d) => d.name)).toEqual(expect.arrayContaining(["example.com", "demo.com"]));
|
||||
// 同步出的域名默认 pending,要逐个「设置 Email Workers」
|
||||
expect(detail.domains.every((d) => d.status === "pending")).toBe(true);
|
||||
|
||||
// token 加密入库,不落明文;provider 标成 cloudflare-token
|
||||
const conn = await t.env.DB.prepare(
|
||||
"SELECT provider, account_id, encrypted_access_token, expires_at FROM oauth_connections WHERE account_id = ?",
|
||||
)
|
||||
.bind(body.account_id)
|
||||
.first<{ provider: string; account_id: string; encrypted_access_token: string; expires_at: string | null }>();
|
||||
expect(conn?.provider).toBe("cloudflare-token");
|
||||
expect(conn!.encrypted_access_token).not.toContain("cf-user-api-token-abc");
|
||||
expect(conn!.expires_at).toBeNull(); // API Token 长期有效
|
||||
});
|
||||
|
||||
test("Token 无效 / 账户 ID 对不上:当场报错,不留下僵尸账号", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
const cf = installCloudflareMock({ zones: [] }); // 一个域名都看不到 = token 没权限
|
||||
mock = cf;
|
||||
|
||||
const before = ((await (await authedFetch(t, cookie, "http://localhost/api/accounts")).json()) as {
|
||||
total: number;
|
||||
}).total;
|
||||
const resp = await authedFetch(t, cookie, "http://localhost/api/accounts/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: "坏 Token", token: "bad-token" }),
|
||||
});
|
||||
expect(resp.status).toBe(400);
|
||||
const body = (await resp.json()) as { error: string };
|
||||
expect(body.error).toContain("API Token 校验失败");
|
||||
const after = ((await (await authedFetch(t, cookie, "http://localhost/api/accounts")).json()) as {
|
||||
total: number;
|
||||
}).total;
|
||||
expect(after).toBe(before); // 没建出账号
|
||||
|
||||
// 账户 ID 与 token 实际所属账户不一致 -> 明确指出两个 ID
|
||||
installCloudflareMock();
|
||||
const mismatch = await authedFetch(t, cookie, "http://localhost/api/accounts/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: "对不上", cloudflare_account_id: "cf-acct-wrong", token: "tok" }),
|
||||
});
|
||||
expect(mismatch.status).toBe(400);
|
||||
const mBody = (await mismatch.json()) as { error: string };
|
||||
expect(mBody.error).toContain("cf-acct-wrong");
|
||||
expect(mBody.error).toContain("cf-acct-real-1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,6 +143,102 @@ for (const path of ["/accounts", "/domains", "/mailboxes", "/inboxs"]) {
|
||||
if (path === "/mailboxes") await page.screenshot({ path: `${SHOT_DIR}/11-batch-selected.png` });
|
||||
}
|
||||
|
||||
// 6b. 添加账号模态窗口:OAuth / API Token 两种授权方式 + 各自说明
|
||||
await page.goto(`${BASE}/accounts`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(".ant-table", { timeout: 8000 });
|
||||
await page.click("button:has-text('添加账号')");
|
||||
await page.waitForSelector(".ant-modal:has-text('添加 Cloudflare 账号')", { timeout: 8000 });
|
||||
const addTabs = await page.locator(".ant-modal .ant-tabs-tab").allInnerTexts();
|
||||
check(
|
||||
"添加账号模态窗口含 OAuth / API Token 两种授权方式",
|
||||
addTabs.some((t) => t.includes("OAuth")) && addTabs.some((t) => t.includes("API Token")),
|
||||
addTabs.join(" | "),
|
||||
);
|
||||
check(
|
||||
"OAuth 方式带说明(不需在本系统保存密钥)",
|
||||
(await page.locator(".ant-modal :text('不需要在本机保存任何密钥')").count()) > 0,
|
||||
);
|
||||
await page.screenshot({ path: `${SHOT_DIR}/12a-add-account-oauth.png` });
|
||||
await page.click(".ant-modal .ant-tabs-tab:has-text('API Token')");
|
||||
await page.waitForSelector(".ant-modal :text('账户名称')", { timeout: 8000 });
|
||||
check(
|
||||
"API Token 方式含 账户名称 / 账户 ID / API Token 三个字段",
|
||||
(await page.locator(".ant-modal :text('账户 ID')").count()) > 0 &&
|
||||
(await page.locator(".ant-modal input[type=password]").count()) > 0,
|
||||
);
|
||||
check(
|
||||
"API Token 方式带说明(会先校验再接入)",
|
||||
(await page.locator(".ant-modal :text('确认可用后才建账号')").count()) > 0,
|
||||
);
|
||||
await page.screenshot({ path: `${SHOT_DIR}/12b-add-account-token.png` });
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// 6c. 添加邮箱模态窗口:域名只列已装 Email Workers 的、随机用户名、catch-all 开关禁用地址
|
||||
await page.goto(`${BASE}/mailboxes`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(".ant-table", { timeout: 8000 });
|
||||
check(
|
||||
"邮箱列表不再有「状态」列(已由「启用」开关承担)",
|
||||
(await page.locator(".ant-table-thead th:has-text('状态')").count()) === 0,
|
||||
);
|
||||
check(
|
||||
"地址列带复制按钮",
|
||||
(await page.locator(".ant-table-tbody button.copy-btn").count()) > 0,
|
||||
);
|
||||
await page.click("button:has-text('添加邮箱')");
|
||||
await page.waitForSelector(".ant-modal:has-text('添加邮箱')", { timeout: 8000 });
|
||||
await page.locator(".ant-modal .ant-select").first().click();
|
||||
await page.waitForTimeout(600);
|
||||
const domainOptions = await page.locator(".ant-select-dropdown .ant-select-item-option-content").allInnerTexts();
|
||||
check("添加邮箱的域名下拉有可选项", domainOptions.length > 0, domainOptions.join(" | "));
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(300);
|
||||
// 随机用户名:点一下应把名字库返回的名字填进地址框
|
||||
await page.click(".ant-modal button:has(.anticon-thunderbolt)");
|
||||
await page.waitForFunction(
|
||||
() => [...document.querySelectorAll(".ant-modal input")].some((i) => (i as HTMLInputElement).value?.length),
|
||||
undefined,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
const addressInput = page.locator(".ant-modal input").nth(1);
|
||||
const filled = (await addressInput.inputValue()).trim();
|
||||
check("随机用户名可从名字库取到并填入地址框", filled.length > 0, filled);
|
||||
// 勾上 catch-all 后地址输入框应被禁用
|
||||
await page.locator(".ant-modal .ant-switch").first().click();
|
||||
await page.waitForTimeout(600);
|
||||
check("勾选 catch-all 后地址输入框被禁用", await addressInput.isDisabled());
|
||||
await page.screenshot({ path: `${SHOT_DIR}/13-add-mailbox.png` });
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// 6d. 「设置 Email Workers」点击后先弹确认框,不直接发请求
|
||||
await page.goto(`${BASE}/domains`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(".ant-table-tbody button", { timeout: 8000 });
|
||||
check(
|
||||
"域名列表不再有「Email Worker」列(统一命名,状态列已足够)",
|
||||
(await page.locator(".ant-table-thead th:has-text('Email Worker')").count()) === 0,
|
||||
);
|
||||
const setupBtn = page
|
||||
.locator(".ant-table-tbody button")
|
||||
.filter({ hasText: /设置 Email Workers|重新配置/ })
|
||||
.first();
|
||||
if ((await setupBtn.count()) > 0) {
|
||||
await setupBtn.click();
|
||||
await page.waitForSelector(".ant-modal-confirm, .ant-popconfirm, .ant-popover", { timeout: 8000 });
|
||||
const confirmText =
|
||||
(await page.locator(".ant-modal-confirm, .ant-popconfirm, .ant-popover").first().innerText().catch(() => "")) ?? "";
|
||||
check(
|
||||
"点「设置 Email Workers」先弹确认框(说明会改 MX 与路由)",
|
||||
confirmText.includes("Email Routing"),
|
||||
confirmText.split("\n")[0],
|
||||
);
|
||||
await page.waitForTimeout(600); // 等确认框进场动画,截图才有内容
|
||||
await page.screenshot({ path: `${SHOT_DIR}/14-setup-confirm.png` });
|
||||
await page.keyboard.press("Escape");
|
||||
} else {
|
||||
check("点「设置 Email Workers」先弹确认框(说明会改 MX 与路由)", false, "找不到配置按钮");
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
|
||||
Reference in New Issue
Block a user