- 검색 그룹테스트 러너 추가 (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>
307 lines
13 KiB
JavaScript
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);
|
|
});
|