UNPKG

@dhlab/e2e-autogen

Version:

Google 스프레드시트 기반 시나리오를 Playwright 테스트 스텁 코드로 자동 생성하고, 테스트 실행 결과를 다시 시트에 업데이트하는 CLI 도구

220 lines (218 loc) 7.73 kB
'use strict'; 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)); }