64 lines
2.9 KiB
TypeScript
64 lines
2.9 KiB
TypeScript
import type { FullConfig, FullResult, Reporter, TestCase, TestResult } from "@playwright/test/reporter";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
type Options = { runId: string };
|
|
|
|
function safeName(value: string) {
|
|
return value.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, "-").slice(0, 100);
|
|
}
|
|
|
|
function relativeTestPath(test: TestCase) {
|
|
const file = test.location.file.replace(/\\/g, "/");
|
|
const marker = "/tests/e2e/";
|
|
const index = file.lastIndexOf(marker);
|
|
return index >= 0 ? file.slice(index + marker.length) : path.basename(file);
|
|
}
|
|
|
|
export default class ChineseFailureReporter implements Reporter {
|
|
private readonly options: Options;
|
|
private config!: FullConfig;
|
|
private failed = 0;
|
|
|
|
constructor(options: Options) {
|
|
this.options = options;
|
|
}
|
|
|
|
onConfigure(config: FullConfig) {
|
|
this.config = config;
|
|
fs.mkdirSync(path.join(projectRoot(config), "test-reports", this.options.runId, "failures"), { recursive: true });
|
|
}
|
|
|
|
onBegin(config: FullConfig) {
|
|
this.config = config;
|
|
fs.mkdirSync(path.join(projectRoot(config), "test-reports", this.options.runId, "failures"), { recursive: true });
|
|
}
|
|
|
|
onTestEnd(test: TestCase, result: TestResult) {
|
|
if (result.status === "passed" || result.status === "skipped") return;
|
|
this.failed += 1;
|
|
const testFile = relativeTestPath(test).replace(/\.ts$/, "");
|
|
const target = path.join(projectRoot(this.config), "test-reports", this.options.runId, "failures", testFile, safeName(test.title));
|
|
fs.mkdirSync(target, { recursive: true });
|
|
const attachments: string[] = [];
|
|
for (const attachment of result.attachments) {
|
|
if (!attachment.path) continue;
|
|
const targetPath = path.join(target, `${safeName(attachment.name)}${path.extname(attachment.path)}`);
|
|
fs.copyFileSync(attachment.path, targetPath);
|
|
attachments.push(path.relative(projectRoot(this.config), targetPath));
|
|
}
|
|
const errors = result.errors.map((error) => error.message ?? error.stack ?? "未知错误").join("\n\n");
|
|
fs.writeFileSync(path.join(target, "失败说明.md"), `# ${test.title}\n\n- 文件:\`${relativeTestPath(test)}\`\n- 项目:${test.parent.project()?.name ?? "未知"}\n- 重试次数:${result.retry}\n- 状态:${result.status}\n\n## 失败原因\n\n${errors || "未捕获到错误文本,请查看 trace。"}\n\n## 附件\n\n${attachments.length ? attachments.map((item) => `- \`${item}\``).join("\n") : "无"}\n`, "utf8");
|
|
}
|
|
|
|
onEnd(result: FullResult) {
|
|
const root = path.join(projectRoot(this.config), "test-reports", this.options.runId);
|
|
fs.mkdirSync(root, { recursive: true });
|
|
fs.writeFileSync(path.join(root, "SUMMARY.md"), `# 第 ${this.options.runId} 轮测试\n\n- 总体状态:${result.status}\n- 失败测试数:${this.failed}\n- 失败详情:${this.failed ? "见 failures/ 目录" : "无"}\n`, "utf8");
|
|
}
|
|
}
|
|
|
|
function projectRoot(config: FullConfig) {
|
|
return path.resolve(config.rootDir, "../..");
|
|
}
|