- 시나리오 품질 감사 리포트 추가 (8개 이슈 유형, 68개 시나리오 분석) - CRUD 수정 스크립트 6개 추가 (DELETE/UPDATE/CREATE 액션 정합성 강화) - 최종 테스트 결과: 68/68 (100%) PASS, 19.6분 소요 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
6.0 KiB
JavaScript
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`);
|
|
}
|