Files
sam-hotfix/e2e/runner/strengthen-crud.js
김보곤 f27fa72c64 test: E2E 시나리오 품질 감사 및 CRUD 강화 - 68/68 PASS (2026-02-11)
- 시나리오 품질 감사 리포트 추가 (8개 이슈 유형, 68개 시나리오 분석)
- CRUD 수정 스크립트 6개 추가 (DELETE/UPDATE/CREATE 액션 정합성 강화)
- 최종 테스트 결과: 68/68 (100%) PASS, 19.6분 소요

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:43:40 +09:00

200 lines
6.0 KiB
JavaScript

/**
* strengthen-crud.js
* CRUD 스텝 강화: click_if_exists(소프트) → click/fill(하드 실패) 변환
*
* 변환 규칙:
* 1. CREATE/UPDATE/DELETE 버튼 클릭 → click (요소 없으면 FAIL)
* 2. CREATE/UPDATE 입력 필드 + value → fill (요소 없으면 FAIL)
* 3. DELETE 확인 다이얼로그 → click_dialog_confirm
* 4. READ 단계 → click_if_exists 유지 (READ는 소프트)
* 5. CREATE 저장 스텝 → critical: true 추가
*/
const fs = require('fs');
const path = require('path');
const SCENARIOS_DIR = path.join(__dirname, '..', 'scenarios');
// 입력 요소 패턴
const INPUT_TARGET_RE = /^(input|textarea|select)\[/i;
const INPUT_TARGET_RE2 = /input\[|textarea\[|select\[/i;
// 버튼 타겟 패턴
const BUTTON_TARGET_RE = /button[:.\[]/i;
// 다이얼로그 패턴
const DIALOG_TARGET_RE = /alertdialog|role=['"]dialog/i;
// 저장 관련 키워드 (CREATE/UPDATE)
const SAVE_KEYWORDS = ['저장', '등록 저장', '등록 완료', '필수 검증'];
function classifyStep(step) {
const target = step.target || '';
const name = step.name || '';
const hasValue = step.value !== undefined;
// 1. 다이얼로그 확인 (DELETE confirm)
if (DIALOG_TARGET_RE.test(target)) {
return 'dialog_confirm';
}
// 2. 입력 필드 + value → fill
if (hasValue && INPUT_TARGET_RE2.test(target) && !BUTTON_TARGET_RE.test(target)) {
return 'input_fill';
}
// 3. 버튼 클릭
if (BUTTON_TARGET_RE.test(target)) {
return 'button_click';
}
// 4. has-text 패턴 (버튼으로 추정)
if (target.includes(':has-text(')) {
return 'button_click';
}
// 5. 이름 기반 추정
if (name.includes('버튼') || name.includes('클릭') || name.includes('저장') ||
name.includes('등록') || name.includes('삭제') || name.includes('수정')) {
if (!hasValue) return 'button_click';
}
return 'unknown';
}
function strengthenScenario(scenario) {
const changes = [];
const steps = scenario.steps || [];
for (const step of steps) {
if (!step.phase) continue;
if (step.action !== 'click_if_exists') continue;
const phase = step.phase;
const classification = classifyStep(step);
// CREATE, UPDATE, DELETE만 강화 (FILTER/SEARCH/SORT/EXPORT는 소프트 유지)
const CRUD_PHASES = new Set(['CREATE', 'UPDATE', 'DELETE']);
if (!CRUD_PHASES.has(phase)) continue;
switch (classification) {
case 'dialog_confirm':
// DELETE 확인 다이얼로그 → click_dialog_confirm
step.action = 'click_dialog_confirm';
changes.push({
stepId: step.id,
phase,
from: 'click_if_exists',
to: 'click_dialog_confirm',
name: step.name
});
break;
case 'input_fill':
// 입력 필드 → fill
step.action = 'fill';
changes.push({
stepId: step.id,
phase,
from: 'click_if_exists',
to: 'fill',
name: step.name
});
break;
case 'button_click':
// 버튼 → click (하드 실패)
step.action = 'click';
// CREATE 저장 스텝에 critical 추가
if (phase === 'CREATE' && SAVE_KEYWORDS.some(kw => step.name.includes(kw))) {
step.critical = true;
changes.push({
stepId: step.id,
phase,
from: 'click_if_exists',
to: 'click + critical:true',
name: step.name
});
} else {
changes.push({
stepId: step.id,
phase,
from: 'click_if_exists',
to: 'click',
name: step.name
});
}
break;
default:
// Unknown → 변환하지 않음
break;
}
}
return changes;
}
// === Main ===
console.log('\n\x1b[1m=== CRUD 스텝 강화 (Strengthen CRUD Steps) ===\x1b[0m');
console.log(`시나리오 경로: ${SCENARIOS_DIR}\n`);
const files = fs.readdirSync(SCENARIOS_DIR)
.filter(f => f.endsWith('.json') && !f.startsWith('_'))
.sort();
let totalChanges = 0;
let modifiedFiles = 0;
const summary = [];
for (const file of files) {
const filePath = path.join(SCENARIOS_DIR, file);
let scenario;
try {
scenario = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch (e) {
console.log(` ⚠️ ${file}: JSON 파싱 오류 - ${e.message}`);
continue;
}
// CRUD phase가 있는 시나리오만 처리
const hasCrudPhases = scenario.steps?.some(s => s.phase && s.phase !== 'READ');
if (!hasCrudPhases) continue;
const changes = strengthenScenario(scenario);
if (changes.length > 0) {
fs.writeFileSync(filePath, JSON.stringify(scenario, null, 2) + '\n');
modifiedFiles++;
totalChanges += changes.length;
const phases = [...new Set(changes.map(c => c.phase))].join(', ');
console.log(`\x1b[36m${file}\x1b[0m: ${changes.length}건 변환 [${phases}]`);
for (const c of changes) {
const arrow = c.to.includes('critical') ? '\x1b[31m→\x1b[0m' : '→';
console.log(` Step ${c.stepId} [${c.phase}] ${c.from} ${arrow} \x1b[32m${c.to}\x1b[0m "${c.name}"`);
}
summary.push({
file,
scenario: scenario.name,
changes: changes.length,
phases,
details: changes
});
}
}
console.log(`\n\x1b[1m=== 완료 ===\x1b[0m`);
console.log(`수정 파일: ${modifiedFiles}`);
console.log(`총 변환: ${totalChanges}`);
console.log(` click_if_exists → click: ${summary.reduce((s, f) => s + f.details.filter(c => c.to === 'click').length, 0)}`);
console.log(` click_if_exists → click + critical: ${summary.reduce((s, f) => s + f.details.filter(c => c.to.includes('critical')).length, 0)}`);
console.log(` click_if_exists → fill: ${summary.reduce((s, f) => s + f.details.filter(c => c.to === 'fill').length, 0)}`);
console.log(` click_if_exists → click_dialog_confirm: ${summary.reduce((s, f) => s + f.details.filter(c => c.to === 'click_dialog_confirm').length, 0)}`);
if (modifiedFiles > 0) {
console.log(`\n다음 단계: node e2e/runner/run-all.js`);
}