You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { test, expect, type Page, type Locator } from "@playwright/test";
import * as fs from "node:fs";
import * as path from "node:path";
type TimingRow = {
step: string;
ms: number;
ok: boolean;
note?: string;
};
const rows: TimingRow[] = [];
/** antd 两字中文按钮常插入空格(登录→登 录) */
function btn(page: Page, label: string): Locator {
const re = new RegExp(`^${label.split("").map(escapeRe).join("\\s*")}$`);
return page.getByRole("button", { name: re });
}
function escapeRe(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
async function timed(
step: string,
fn: () => Promise<string | void>,
): Promise<void> {
const t0 = Date.now();
try {
const note = (await fn()) || undefined;
rows.push({ step, ms: Date.now() - t0, ok: true, note });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
rows.push({ step, ms: Date.now() - t0, ok: false, note: msg.slice(0, 240) });
throw e;
}
}
async function waitQuiet(page: Page, ms = 150) {
await page.waitForLoadState("domcontentloaded").catch(() => undefined);
await page.waitForTimeout(ms);
}
async function login(page: Page) {
await page.goto("/login");
await expect(btn(page, "登录")).toBeVisible();
// Form 已有 initialValues;再 fill 会叠字
const user = page.getByLabel("用户名");
await user.click();
await user.fill("");
await user.fill("admin");
const pwd = page.getByLabel("密码");
await pwd.click();
await pwd.fill("");
await pwd.fill("admin123");
await btn(page, "登录").click();
await expect(page).toHaveURL(/\/mails/, { timeout: 20_000 });
await expect(page.getByRole("heading", { name: "邮件预报" })).toBeVisible({
timeout: 20_000,
});
}
test.describe("UX timing smoke", () => {
test("modules + buttons", async ({ page }) => {
test.setTimeout(240_000);
rows.length = 0;
const writeReport = () => {
const outDir = path.join(process.cwd(), "data", "logs");
fs.mkdirSync(outDir, { recursive: true });
const total = rows.reduce((s, r) => s + r.ms, 0);
const summary = {
at: new Date().toISOString(),
total_ms: total,
fail: rows.filter((r) => !r.ok).length,
rows,
};
fs.writeFileSync(
path.join(outDir, "ux-timing-smoke.json"),
JSON.stringify(summary, null, 2),
"utf8",
);
const md = [
"# UX Timing Smoke",
"",
`| step | ms | ok | note |`,
`|---|---:|:---:|---|`,
...rows.map(
(r) =>
`| ${r.step} | ${r.ms} | ${r.ok ? "Y" : "N"} | ${(r.note ?? "").replace(/\|/g, "/")} |`,
),
"",
`**total**: ${total} ms`,
"",
`**判定**:单步导航 <1.5s 优,<3s 可;刷新/查询 <2s 优;弹窗开闭 <1s 优`,
].join("\n");
fs.writeFileSync(path.join(outDir, "ux-timing-smoke.md"), md, "utf8");
};
try {
await timed("01 打开登录页", async () => {
await page.goto("/login");
await expect(btn(page, "登录")).toBeVisible({ timeout: 15_000 });
});
await timed("02 登录 → /mails", async () => {
await login(page);
return page.url();
});
await timed("03 邮件列表:刷新", async () => {
const refreshBtn = page.getByRole("button", { name: /刷\s*新/ }).first();
await expect(refreshBtn).toBeVisible({ timeout: 20_000 });
// 等非 loading 再点,避免叠请求
await expect(
page.getByRole("button", { name: /^刷\s*新$/ }),
).toBeVisible({ timeout: 30_000 });
const waitApi = page.waitForResponse(
(r) =>
r.url().includes("/api/mails") &&
r.request().method() === "GET" &&
r.ok(),
{ timeout: 20_000 },
);
await page.getByRole("button", { name: /^刷\s*新$/ }).click();
const res = await waitApi;
await expect(
page.getByRole("button", { name: /^刷\s*新$/ }),
).toBeVisible({ timeout: 30_000 });
return `status=${res.status()}`;
});
await timed("04 邮件列表:查询", async () => {
await expect(page.getByRole("button", { name: /^查\s*询$/ })).toBeVisible({
timeout: 20_000,
});
await page.getByPlaceholder("来源 / 主题 / 正文").fill("MATU");
const wait1 = page.waitForResponse(
(r) =>
r.url().includes("/api/mails") &&
r.request().method() === "GET" &&
r.status() === 200,
{ timeout: 20_000 },
);
await page.getByRole("button", { name: /^查\s*询$/ }).click();
await wait1;
await expect(page.getByRole("button", { name: /^查\s*询$/ })).toBeVisible({
timeout: 20_000,
});
await page.getByPlaceholder("来源 / 主题 / 正文").fill("");
const wait2 = page.waitForResponse(
(r) =>
r.url().includes("/api/mails") &&
r.request().method() === "GET" &&
r.status() === 200,
{ timeout: 20_000 },
);
await page.getByRole("button", { name: /^查\s*询$/ }).click();
await wait2;
await expect(page.getByRole("button", { name: /^查\s*询$/ })).toBeVisible({
timeout: 20_000,
});
});
await timed("05 邮件列表:类型筛选", async () => {
await page.getByRole("combobox").nth(0).click();
await page.getByTitle("新增预报").click();
await waitQuiet(page);
const clear = page.locator(".ant-select-clear").first();
if (await clear.isVisible().catch(() => false)) await clear.click();
await waitQuiet(page);
});
await timed("06 导航:邮件列表 → 日志", async () => {
await page.getByRole("link", { name: "日志" }).click();
await expect(page).toHaveURL(/\/logs/);
await expect(page.getByText("日志与审计")).toBeVisible({
timeout: 15_000,
});
await waitQuiet(page);
});
await timed("07 日志:查询", async () => {
const query = btn(page, "查询").first();
await expect(query).toBeVisible();
await query.click();
await waitQuiet(page);
});
await timed("08 日志:切操作审计 Tab", async () => {
await page.getByRole("tab", { name: /操作审计/ }).click();
await waitQuiet(page);
await page.getByRole("tab", { name: "导入日志" }).click();
await waitQuiet(page);
});
await timed("09 导航:日志 → 设置", async () => {
await page.getByRole("link", { name: "设置" }).click();
await expect(page).toHaveURL(/\/settings/);
await expect(page.getByText("邮箱绑定")).toBeVisible({ timeout: 15_000 });
await waitQuiet(page);
});
await timed("10 设置:打开授权码绑定弹窗并取消", async () => {
await page.getByRole("button", { name: "授权码绑定" }).click();
await expect(page.getByRole("dialog")).toBeVisible();
await btn(page, "取消").click();
await expect(page.getByRole("dialog")).toBeHidden({ timeout: 5_000 });
});
await timed("11 设置:打开 OAuth 配置弹窗并关闭", async () => {
const oauthBtn = page.getByRole("button", { name: /配置 OAuth/ });
await expect(oauthBtn).toBeVisible();
await oauthBtn.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
// antd Modal:优先点取消;Escape 在部分版本不关
const cancel = dialog.getByRole("button", { name: /^取\s*消$|^Cancel$/ });
if (await cancel.isVisible().catch(() => false)) {
await cancel.click();
} else {
await dialog.locator(".ant-modal-close").click();
}
await expect(dialog).toBeHidden({ timeout: 5_000 });
});
await timed("12 设置:CC 测试登录(mock)", async () => {
const ccTest = page.getByRole("button", { name: /测试\s*登录/ });
await expect(ccTest).toBeVisible();
await ccTest.click();
await waitQuiet(page, 500);
});
await timed("13 导航:设置 → 邮件列表", async () => {
await page.getByRole("link", { name: "邮件列表" }).click();
await expect(page).toHaveURL(/\/mails$/);
await expect(page.getByRole("heading", { name: "邮件预报" })).toBeVisible({
timeout: 15_000,
});
await waitQuiet(page);
});
await timed("14 打开第一封邮件详情", async () => {
const detail = page.getByRole("button", { name: "详情" }).first();
await expect(detail).toBeVisible({ timeout: 15_000 });
await detail.click();
await expect(page).toHaveURL(/\/mails\/[^/]+$/);
await expect(page.getByRole("button", { name: /回列表/ })).toBeVisible({
timeout: 15_000,
});
await waitQuiet(page);
});
await timed("15 详情:打开改类型弹窗并取消", async () => {
const b = page.getByRole("button", { name: /改\s*类型/ });
if (!(await b.isVisible().catch(() => false))) {
return "skip: no override button";
}
await b.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
const cancel = dialog.getByRole("button", { name: /^取\s*消$|^Cancel$/ });
if (await cancel.isVisible().catch(() => false)) {
await cancel.click();
} else {
await dialog.locator(".ant-modal-close").click();
}
await expect(dialog).toBeHidden({ timeout: 5_000 });
});
await timed("16 详情:回列表", async () => {
await page.getByRole("button", { name: /回列表/ }).click();
await expect(page).toHaveURL(/\/mails$/);
await waitQuiet(page);
});
await timed("17 模块来回切换 x3(mails↔logs↔settings)", async () => {
const t0 = Date.now();
for (let i = 0; i < 3; i++) {
await page.getByRole("link", { name: "日志" }).click();
await expect(page).toHaveURL(/\/logs/);
await page.getByRole("link", { name: "设置" }).click();
await expect(page).toHaveURL(/\/settings/);
await page.getByRole("link", { name: "邮件列表" }).click();
await expect(page).toHaveURL(/\/mails$/);
}
await waitQuiet(page);
return `avg_roundtrip≈${Math.round((Date.now() - t0) / 3)}ms`;
});
await timed("18 退出登录", async () => {
await btn(page, "退出").click();
await expect(page).toHaveURL(/\/login/, { timeout: 15_000 });
});
} finally {
writeReport();
}
expect(rows.every((r) => r.ok)).toBeTruthy();
});
});