@dhlab/e2e-autogen
Version:
Google 스프레드시트 기반 시나리오를 Playwright 테스트 스텁 코드로 자동 생성하고, 테스트 실행 결과를 다시 시트에 업데이트하는 CLI 도구
218 lines (217 loc) • 7.71 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs_1 = tslib_1.__importDefault(require("fs"));
const path_1 = tslib_1.__importDefault(require("path"));
class E2EAutogenPlaywrightReporter {
constructor(options) {
this.suites = [];
this.outputFile = options?.outputFile
? path_1.default.resolve(process.cwd(), options.outputFile)
: path_1.default.resolve(process.cwd(), "e2e-autogen-report.json");
}
onBegin(config, suite) {
this.suites = [];
}
async onTestEnd(test, result) {
const file = test.location.file;
const suiteTitle = test.parent?.title ?? "";
const scenarioId = extractScenarioId(test.title);
const specTitle = test.titlePath().find((t) => t.includes("TC-")) || test.title;
const description = removeTestIdFromTitle(specTitle);
const manualSteps = extractManualSteps(result).filter((s) => s.testId.startsWith(scenarioId));
// Step 분석
const flakySteps = getFlakyStepTitles(result);
const steps = (result.steps ?? [])
.map((step) => {
const testId = extractTestId(step.title);
const duration = step.duration;
const error = step.error?.message;
if (!testId) {
return null;
}
const manualMatch = manualSteps.find((m) => m.testId === testId);
if (manualMatch) {
return {
title: step.title,
testId,
duration,
...(error ? { error } : {}),
status: "manual_only",
};
}
if (flakySteps.includes(step.title)) {
return {
title: step.title,
testId,
duration,
...(error ? { error } : {}),
status: "flaky",
};
}
if (duration === 0) {
return {
title: step.title,
testId,
duration,
...(error ? { error } : {}),
status: "not_executed",
};
}
return {
title: step.title,
testId,
duration,
...(error ? { error } : {}),
status: error ? "fail" : "pass",
};
})
.filter((s) => s !== null);
const entry = {
title: test.title,
status: result.status,
duration: result.duration,
error: result.error?.message,
steps,
};
let suiteGroup = this.suites.find((s) => s.file === file);
if (!suiteGroup) {
suiteGroup = { title: suiteTitle, file, specs: [] };
this.suites.push(suiteGroup);
}
let spec = suiteGroup.specs.find((s) => s.scenarioId === scenarioId);
if (!spec) {
spec = {
title: specTitle,
scenarioId,
description,
file,
tests: [],
pass: [],
fail: [],
flaky: [],
manual_only: [],
not_executed: [],
};
suiteGroup.specs.push(spec);
}
spec.tests.push(entry);
// ----- manual_only 기록 -----
for (const step of steps) {
if (step.status !== "manual_only" || !step.testId)
continue;
const reason = manualSteps.find((m) => m.testId === step.testId)?.description ?? "";
const already = spec.manual_only.find((m) => m[0] === step.testId);
if (!already)
spec.manual_only.push([step.testId, reason]);
}
}
async onEnd() {
// 최종 분류 계산
for (const suite of this.suites) {
for (const spec of suite.specs) {
updateSpecClassification(spec);
}
}
fs_1.default.writeFileSync(this.outputFile, JSON.stringify({ suites: this.suites }, null, 2));
}
printsToStdio() {
return true;
}
}
exports.default = E2EAutogenPlaywrightReporter;
// ───── 유틸 함수들 ─────
function extractTestId(title) {
const match = title.match(/\[(TC-[\d.]+)]/);
return match ? match[1] : "";
}
function extractScenarioId(title) {
const match = title.match(/\[(TC-\d+\.\d+)/);
return match ? match[1] : "";
}
function removeTestIdFromTitle(title) {
return title.replace(/\[TC-[\d.]+]\s*/, "").trim();
}
function getFlakyStepTitles(result) {
const map = {};
const retryDetails = result.retryDetails ?? [];
for (const attempt of retryDetails) {
for (const step of attempt.steps) {
if (!map[step.title])
map[step.title] = [];
map[step.title].push(step.error ? "fail" : "pass");
}
}
for (const step of result.steps ?? []) {
if (!map[step.title])
map[step.title] = [];
map[step.title].push(step.error ? "fail" : "pass");
}
return Object.entries(map)
.filter(([, statuses]) => statuses.includes("fail") && statuses.includes("pass"))
.map(([title]) => title);
}
/**
* result.annotations 에 기록된 manual_only 정보를 추출한다.
* description 형식: `${testId}|${reason}`
*/
function extractManualSteps(result) {
const annotations = result.annotations ?? [];
return annotations
.filter((a) => a.type === "manual_only")
.map((a) => {
const [testId, ...descParts] = (a.description ?? "").split("|");
return { testId, description: descParts.join("|") };
})
.filter((m) => m.testId);
}
function updateSpecClassification(spec) {
// 초기화
spec.pass = [];
spec.fail = [];
spec.flaky = [];
spec.not_executed = [];
const stepStatusMap = {};
// 시도별 Step 상태 수집
for (const testEntry of spec.tests) {
for (const step of testEntry.steps) {
if (!step.testId)
continue;
if (!stepStatusMap[step.testId])
stepStatusMap[step.testId] = [];
stepStatusMap[step.testId].push(step.status);
}
}
const addUnique = (arr, id) => {
if (!arr.includes(id))
arr.push(id);
};
for (const [testId, statuses] of Object.entries(stepStatusMap)) {
const uniqStatuses = Array.from(new Set(statuses));
// 1) flaky 위치가 최우선 (시도 내 pass+fail 혼재 또는 step 자체가 flaky)
if (uniqStatuses.includes("flaky")) {
addUnique(spec.flaky, testId);
continue;
}
const hasPass = uniqStatuses.includes("pass");
const hasFail = uniqStatuses.includes("fail");
const hasNotExecuted = uniqStatuses.includes("not_executed");
if (hasPass && hasFail) {
// 재시도 혼합 결과
addUnique(spec.flaky, testId);
}
else if (hasFail) {
addUnique(spec.fail, testId);
}
else if (hasPass) {
addUnique(spec.pass, testId);
}
else if (hasNotExecuted) {
addUnique(spec.not_executed, testId);
}
}
// 중복 방지 보증 (예외 케이스 대비)
spec.pass = Array.from(new Set(spec.pass));
spec.fail = Array.from(new Set(spec.fail));
spec.flaky = Array.from(new Set(spec.flaky));
spec.not_executed = Array.from(new Set(spec.not_executed));
}