Files
sam-hotfix/e2e/runner/run-search-group.js
김보곤 b7cbb5c79f test: E2E 전체 테스트 206/206 ALL PASS + 검색 그룹테스트 러너 추가
- 검색 그룹테스트 러너 추가 (run-search-group.js): 24개 검색 시나리오를 6개 카테고리로 분류 실행
- 검색 그룹테스트 24/24 ALL PASS (23.6분)
- 전체 E2E 테스트 206/206 ALL PASS (88.9분)
- 카테고리: 접근성(18), 기능(149), 엣지케이스(17), 성능(17), 워크플로우(5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:51:04 +09:00

307 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* 검색 로직 그룹테스트 러너
* - 단일 브라우저 세션으로 검색 시나리오 일괄 실행 (속도 최적화)
* - run-all.js --filter 로 한 번에 실행 → 결과를 카테고리별로 분류
*
* Usage:
* node e2e/runner/run-search-group.js # full (23개 전체)
* node e2e/runner/run-search-group.js --level quick # bug+function (5개)
* node e2e/runner/run-search-group.js --level standard # 16개 (감사 제외)
* node e2e/runner/run-search-group.js --level full # 23개 전체
* node e2e/runner/run-search-group.js --group audit # 감사 6개
* node e2e/runner/run-search-group.js --group options # 옵션 11개
* node e2e/runner/run-search-group.js --group bug # 버그 2개
* node e2e/runner/run-search-group.js --group function # 기능 3개
* node e2e/runner/run-search-group.js --group filter # 필터 1개
* node e2e/runner/run-search-group.js --headless # 백그라운드
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// ─── Config ─────────────────────────────────────────────
const CONFIG_PATH = path.join(__dirname, '..', 'scenarios', '_search-group-config.json');
const RUNNER_PATH = path.join(__dirname, 'run-all.js');
const RESULTS_DIR = path.join(__dirname, '..', 'results', 'hotfix');
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
// ─── CLI Args ───────────────────────────────────────────
const args = process.argv.slice(2);
const HEADLESS = !args.includes('--headed'); // 기본 headless
const LEVEL = (() => {
const idx = args.indexOf('--level');
return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
})();
const GROUP = (() => {
const idx = args.indexOf('--group');
return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
})();
// ─── Group → Filter Prefix Mapping ─────────────────────
// 단일 --filter 호출로 최대한 매칭, 결과에서 카테고리 분류
const GROUP_FILTER_MAP = {
bug: 'search-bug',
edge: 'edge-special-chars-search',
function: 'search-function', // search-function-audit 포함되지만 결과에서 분리
filter: 'search-filter',
audit: 'search-function-audit',
options: 'search-options'
};
// ─── Resolve Target ─────────────────────────────────────
function resolveTarget() {
if (GROUP) {
const g = config.groups[GROUP];
if (!g) {
console.error(`\x1b[31m알 수 없는 그룹: ${GROUP}\x1b[0m`);
console.error(`사용 가능: ${Object.keys(config.groups).join(', ')}`);
process.exit(1);
}
return {
filterPrefix: GROUP_FILTER_MAP[GROUP],
targetIds: new Set(g.scenarios),
label: g.name,
description: g.description,
estimatedTime: null
};
}
const level = LEVEL || 'full';
const lv = config.levels[level];
if (!lv) {
console.error(`\x1b[31m알 수 없는 레벨: ${level}\x1b[0m`);
console.error(`사용 가능: ${Object.keys(config.levels).join(', ')}`);
process.exit(1);
}
const targetIds = new Set();
for (const groupName of lv.include) {
config.groups[groupName].scenarios.forEach(s => targetIds.add(s));
}
return {
filterPrefix: 'search', // 항상 전체 search-* 실행 (단일 브라우저 세션)
targetIds,
label: lv.name,
description: lv.description,
estimatedTime: lv.estimatedTime
};
}
// ─── Parse run-all.js Output ────────────────────────────
function parseRunnerOutput(output) {
const results = [];
// ANSI 이스케이프 코드 제거
const clean = output.replace(/\x1b\[[0-9;]*m/g, '');
const lines = clean.split('\n');
for (const line of lines) {
// 패턴: (N/M) scenario-id ... PASS (X/Y, Z.Zs) 또는 FAIL
const m = line.match(/\(\d+\/\d+\)\s+([\w-]+)\s+\.\.\.\s+(PASS|FAIL)\s*\(?([\d/]+)?(?:,\s*([\d.]+)s)?\)?/);
if (m) {
results.push({
id: m[1],
status: m[2],
steps: m[3] || '',
time: m[4] || '0'
});
}
}
return results;
}
// ─── Categorize Result ──────────────────────────────────
function categorize(scenarioId) {
for (const [gName, g] of Object.entries(config.groups)) {
if (g.scenarios.includes(scenarioId)) return gName;
}
return 'unknown';
}
// ─── Timestamp ──────────────────────────────────────────
function getTimestamp() {
const n = new Date();
const p = v => v.toString().padStart(2, '0');
return `${n.getFullYear()}-${p(n.getMonth() + 1)}-${p(n.getDate())}_${p(n.getHours())}-${p(n.getMinutes())}-${p(n.getSeconds())}`;
}
// ─── Main ───────────────────────────────────────────────
async function main() {
const { filterPrefix, targetIds, label, description, estimatedTime } = resolveTarget();
const ts = getTimestamp();
console.log('\x1b[1m');
console.log('╔══════════════════════════════════════════════════════╗');
console.log('║ 🔍 검색 로직 그룹테스트 러너 ║');
console.log('╚══════════════════════════════════════════════════════╝');
console.log('\x1b[0m');
console.log(` 모드: \x1b[36m${label}\x1b[0m`);
console.log(` 설명: ${description}`);
console.log(` 대상: \x1b[33m${targetIds.size}\x1b[0m 시나리오`);
console.log(` 필터: --filter ${filterPrefix}`);
if (estimatedTime) console.log(` 예상: ~${estimatedTime}`);
console.log(` 실행: ${HEADLESS ? 'headless' : 'headed'} (단일 브라우저 세션)`);
console.log('');
// Print target scenarios grouped
const groupOrder = ['bug', 'edge', 'function', 'filter', 'audit', 'options'];
for (const gName of groupOrder) {
const g = config.groups[gName];
const matching = g.scenarios.filter(s => targetIds.has(s));
if (matching.length === 0) continue;
console.log(` \x1b[2m[${g.name}]\x1b[0m`);
for (const s of matching) {
console.log(` - ${s}`);
}
}
console.log('');
console.log('─'.repeat(56));
console.log('');
// ─── Run via single run-all.js invocation ─────────
const startTime = Date.now();
const headArg = HEADLESS ? '--headless' : '';
const cmd = `node "${RUNNER_PATH}" --filter ${filterPrefix} ${headArg}`;
console.log(` \x1b[2m실행: ${cmd}\x1b[0m`);
console.log('');
let output = '';
try {
output = execSync(cmd, {
encoding: 'utf8',
timeout: 1800000, // 30분
stdio: ['pipe', 'pipe', 'pipe']
});
process.stdout.write(output);
} catch (err) {
output = (err.stdout || '') + (err.stderr || '');
process.stdout.write(output);
}
const totalTime = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
// ─── Parse Results ────────────────────────────────
const allResults = parseRunnerOutput(output);
// Filter to only target scenarios (for levels that run more than needed)
const results = allResults.filter(r => targetIds.has(r.id));
const extraResults = allResults.filter(r => !targetIds.has(r.id));
const passCount = results.filter(r => r.status === 'PASS').length;
const failCount = results.filter(r => r.status === 'FAIL').length;
const passRate = results.length > 0 ? ((passCount / results.length) * 100).toFixed(0) : '0';
// ─── Console Summary ─────────────────────────────
console.log('');
console.log('\x1b[1m');
console.log('╔══════════════════════════════════════════════════════╗');
console.log('║ 🔍 검색 그룹테스트 결과 ║');
console.log('╚══════════════════════════════════════════════════════╝');
console.log('\x1b[0m');
console.log(` 대상: ${results.length}개 | \x1b[32m성공: ${passCount}\x1b[0m | \x1b[${failCount > 0 ? '31' : '2'}m실패: ${failCount}\x1b[0m | 성공률: ${passRate}%`);
if (extraResults.length > 0) {
console.log(` \x1b[2m(추가 실행: ${extraResults.length}개 - 필터 범위 초과분)\x1b[0m`);
}
console.log(` 소요 시간: ${totalTime}`);
if (failCount > 0) {
console.log('');
console.log(' \x1b[31m실패 시나리오:\x1b[0m');
for (const r of results.filter(r => r.status === 'FAIL')) {
console.log(`${r.id} (${r.steps}, ${r.time}s)`);
}
}
// ─── Generate Report ──────────────────────────────
const reportName = `Search-Group-Test_${ts}.md`;
const reportPath = path.join(RESULTS_DIR, reportName);
let report = `# 🔍 검색 로직 그룹테스트 결과\n\n`;
report += `**실행**: ${ts} | **모드**: ${label} | **결과**: ${failCount === 0 ? '✅ ALL PASS' : `${failCount}건 실패`}\n`;
report += `**소요 시간**: ${totalTime}분 | **필터**: \`--filter ${filterPrefix}\`\n\n`;
report += `## 요약\n`;
report += `| 전체 | 성공 | 실패 | 성공률 |\n`;
report += `|------|------|------|--------|\n`;
report += `| ${results.length} | ${passCount} | ${failCount} | ${passRate}% |\n\n`;
report += `## 카테고리별 결과\n\n`;
for (const gName of groupOrder) {
const g = config.groups[gName];
const matching = results.filter(r => g.scenarios.includes(r.id));
if (matching.length === 0) continue;
const gPass = matching.filter(r => r.status === 'PASS').length;
const gFail = matching.filter(r => r.status === 'FAIL').length;
const icon = gFail > 0 ? '❌' : '✅';
report += `### ${icon} ${g.name} (${gPass}/${matching.length})\n`;
report += `| 시나리오 | 상태 | 스텝 | 소요시간 |\n`;
report += `|---------|------|------|----------|\n`;
for (const r of matching) {
const st = r.status === 'PASS' ? '✅' : '❌';
report += `| ${r.id} | ${st} | ${r.steps} | ${r.time}s |\n`;
}
report += '\n';
}
if (extraResults.length > 0) {
report += `### 📎 추가 실행 (필터 범위 초과)\n`;
report += `| 시나리오 | 상태 | 스텝 | 소요시간 |\n`;
report += `|---------|------|------|----------|\n`;
for (const r of extraResults) {
const st = r.status === 'PASS' ? '✅' : '❌';
report += `| ${r.id} | ${st} | ${r.steps} | ${r.time}s |\n`;
}
report += '\n';
}
// Coverage
report += `## 검색 커버리지\n\n`;
report += `| 모듈 | 페이지 수 | 시나리오 수 |\n`;
report += `|------|----------|------------|\n`;
for (const [mod, info] of Object.entries(config.coverage.modules)) {
const coveredInRun = info.scenarios.filter(s => targetIds.has(s)).length;
if (coveredInRun > 0) {
report += `| ${mod} | ${info.pages} | ${coveredInRun} |\n`;
}
}
report += `\n**총 커버 페이지**: ${config.coverage.totalPages}\n`;
if (failCount > 0) {
report += `\n## 실패 분석 필요\n\n`;
for (const r of results.filter(r => r.status === 'FAIL')) {
report += `- **${r.id}**: \`e2e/results/hotfix/Fail-${r.id}_*.md\` 확인\n`;
}
}
report += `\n## 사용법\n\n`;
report += '```bash\n';
report += '# 빠른 검증 (bug+function, 5개)\n';
report += 'node e2e/runner/run-search-group.js --level quick\n\n';
report += '# 표준 테스트 (감사 제외, 16개)\n';
report += 'node e2e/runner/run-search-group.js --level standard\n\n';
report += '# 전체 테스트 (23개)\n';
report += 'node e2e/runner/run-search-group.js --level full\n\n';
report += '# 특정 그룹만\n';
report += 'node e2e/runner/run-search-group.js --group bug\n';
report += 'node e2e/runner/run-search-group.js --group options\n';
report += 'node e2e/runner/run-search-group.js --group audit\n';
report += '```\n';
fs.writeFileSync(reportPath, report, 'utf8');
console.log('');
console.log(` 리포트: ${reportPath}`);
console.log('');
process.exit(failCount > 0 ? 1 : 0);
}
main().catch(err => {
console.error('\x1b[31m치명적 오류:\x1b[0m', err.message);
process.exit(1);
});