|
|
import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
|
|
|
import * as fs from "node:fs";
|
|
|
import * as path from "node:path";
|
|
|
import { execSync } from "node:child_process";
|
|
|
|
|
|
type MailItem = {
|
|
|
key: string;
|
|
|
id: string;
|
|
|
status: string;
|
|
|
mail_type: string;
|
|
|
container_no: string | null;
|
|
|
shipments: number;
|
|
|
table_rows: number;
|
|
|
url: string;
|
|
|
};
|
|
|
|
|
|
type DetailResult = {
|
|
|
key: string;
|
|
|
id: string;
|
|
|
url: string;
|
|
|
open_ms: number;
|
|
|
ok: boolean;
|
|
|
antd_warnings: string[];
|
|
|
page_errors: string[];
|
|
|
visible_checks: string[];
|
|
|
note?: string;
|
|
|
};
|
|
|
|
|
|
function btn(page: Page, label: string) {
|
|
|
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 login(page: Page) {
|
|
|
await page.goto("/login");
|
|
|
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 });
|
|
|
}
|
|
|
|
|
|
function loadSampleItems(): MailItem[] {
|
|
|
execSync("pnpm exec tsx scripts/ingest-sample-mails.ts", {
|
|
|
cwd: process.cwd(),
|
|
|
encoding: "utf8",
|
|
|
env: process.env,
|
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
|
});
|
|
|
const report = path.join(process.cwd(), "data", "logs", "sample-mails-ingest.json");
|
|
|
const json = JSON.parse(fs.readFileSync(report, "utf8")) as { items: MailItem[] };
|
|
|
return json.items;
|
|
|
}
|
|
|
|
|
|
test.describe("sample mails M1-M4 detail console", () => {
|
|
|
test("open four details + catch antd warnings", async ({ page }) => {
|
|
|
test.setTimeout(240_000);
|
|
|
const items = loadSampleItems();
|
|
|
expect(items.length).toBe(4);
|
|
|
|
|
|
await login(page);
|
|
|
|
|
|
const results: DetailResult[] = [];
|
|
|
|
|
|
for (const item of items) {
|
|
|
const antd_warnings: string[] = [];
|
|
|
const page_errors: string[] = [];
|
|
|
const onConsole = (msg: ConsoleMessage) => {
|
|
|
const text = msg.text();
|
|
|
if (
|
|
|
msg.type() === "error" ||
|
|
|
msg.type() === "warning" ||
|
|
|
text.includes("[antd:")
|
|
|
) {
|
|
|
if (text.includes("[antd:")) antd_warnings.push(text.slice(0, 300));
|
|
|
else if (msg.type() === "error") page_errors.push(text.slice(0, 300));
|
|
|
}
|
|
|
};
|
|
|
const onPageError = (err: Error) => {
|
|
|
page_errors.push(err.message.slice(0, 300));
|
|
|
};
|
|
|
page.on("console", onConsole);
|
|
|
page.on("pageerror", onPageError);
|
|
|
|
|
|
const t0 = Date.now();
|
|
|
let ok = true;
|
|
|
const visible_checks: string[] = [];
|
|
|
let note: string | undefined;
|
|
|
try {
|
|
|
await page.goto(`/mails/${item.id}`, { waitUntil: "domcontentloaded" });
|
|
|
await expect(page.getByRole("button", { name: /回列表/ })).toBeVisible({
|
|
|
timeout: 30_000,
|
|
|
});
|
|
|
visible_checks.push("回列表");
|
|
|
|
|
|
await expect(page.locator(".mail-biz-units")).toBeVisible({
|
|
|
timeout: 15_000,
|
|
|
});
|
|
|
visible_checks.push("指令区");
|
|
|
|
|
|
// 若有标准化表,确认 Table 渲染且无 rowKey/span 告警
|
|
|
const tableTitle = page.getByText(/工单标准化列表|货件标准表/);
|
|
|
if (await tableTitle.first().isVisible().catch(() => false)) {
|
|
|
visible_checks.push("标准表");
|
|
|
await expect(page.locator(".ant-table-tbody").first()).toBeVisible();
|
|
|
}
|
|
|
|
|
|
// 等一拍收集异步 warning
|
|
|
await page.waitForTimeout(800);
|
|
|
} catch (e) {
|
|
|
ok = false;
|
|
|
note = e instanceof Error ? e.message.slice(0, 240) : String(e);
|
|
|
} finally {
|
|
|
page.off("console", onConsole);
|
|
|
page.off("pageerror", onPageError);
|
|
|
}
|
|
|
|
|
|
// 过滤已知无关噪声,只关心 antd Descriptions/Table
|
|
|
const critical = antd_warnings.filter(
|
|
|
(w) =>
|
|
|
w.includes("[antd: Descriptions]") ||
|
|
|
w.includes("[antd: Table]") ||
|
|
|
w.includes("rowKey") ||
|
|
|
w.includes("span"),
|
|
|
);
|
|
|
|
|
|
results.push({
|
|
|
key: item.key,
|
|
|
id: item.id,
|
|
|
url: item.url,
|
|
|
open_ms: Date.now() - t0,
|
|
|
ok: ok && critical.length === 0 && page_errors.length === 0,
|
|
|
antd_warnings: critical,
|
|
|
page_errors,
|
|
|
visible_checks,
|
|
|
note:
|
|
|
note ||
|
|
|
`type=${item.mail_type} status=${item.status} cn=${item.container_no} rows=${item.table_rows}`,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const outDir = path.join(process.cwd(), "data", "logs");
|
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
|
const summary = {
|
|
|
at: new Date().toISOString(),
|
|
|
fail: results.filter((r) => !r.ok).length,
|
|
|
results,
|
|
|
};
|
|
|
fs.writeFileSync(
|
|
|
path.join(outDir, "sample-mails-detail-smoke.json"),
|
|
|
JSON.stringify(summary, null, 2),
|
|
|
"utf8",
|
|
|
);
|
|
|
const md = [
|
|
|
"# Sample Mails Detail Smoke (M1–M4)",
|
|
|
"",
|
|
|
`| mail | id | open_ms | ok | antd | checks | note |`,
|
|
|
`|---|---|---:|:---:|---|---|---|`,
|
|
|
...results.map(
|
|
|
(r) =>
|
|
|
`| M${r.key} | ${r.id} | ${r.open_ms} | ${r.ok ? "Y" : "N"} | ${r.antd_warnings.length} | ${r.visible_checks.join("/") || "-"} | ${(r.note ?? "").replace(/\|/g, "/")} |`,
|
|
|
),
|
|
|
"",
|
|
|
...results
|
|
|
.filter((r) => r.antd_warnings.length || r.page_errors.length)
|
|
|
.flatMap((r) => [
|
|
|
`## M${r.key} issues`,
|
|
|
...r.antd_warnings.map((w) => `- antd: ${w}`),
|
|
|
...r.page_errors.map((w) => `- error: ${w}`),
|
|
|
"",
|
|
|
]),
|
|
|
].join("\n");
|
|
|
fs.writeFileSync(path.join(outDir, "sample-mails-detail-smoke.md"), md, "utf8");
|
|
|
|
|
|
expect(results.every((r) => r.ok)).toBeTruthy();
|
|
|
});
|
|
|
});
|