- evaluate 핸들러: ctx.vars(undefined) → ctx.variables 수정 - 기존: TypeError가 inner catch에 흡수되어 항상 "evaluate ok" 반환 - 수정: JSON 파싱 결과를 정확히 분석 (ok:false → fail, grade → details) - gen-detail-roundtrip.js: 상세 조회 왕복 검증 (목록→상세→목록 무결성) - gen-cross-module.js: 모듈 간 데이터 일관성 (판매↔회계, 판매↔생산) - gen-api-health.js v2.0: 내장 ApiMonitor + Performance API 하이브리드 - 전체 120개 시나리오: 113 PASS / 7 FAIL (버그 수정으로 숨겨진 실패 노출) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
219 lines
12 KiB
JavaScript
219 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 모듈 간 데이터 연동 검증 시나리오 생성기
|
|
*
|
|
* 비파괴적 교차 확인: 한 모듈의 데이터가 다른 모듈에서도 동일하게 존재하는지 검증
|
|
*
|
|
* 흐름:
|
|
* 1. 판매>거래처관리에서 첫 거래처명 캡처
|
|
* 2. 회계>거래처관리로 이동 → 동일 거래처 존재 확인
|
|
* 3. 판매>단가관리에서 품목명 캡처
|
|
* 4. 생산>품목관리로 이동 → 동일 품목 존재 확인
|
|
*
|
|
* Usage: node e2e/runner/gen-cross-module.js
|
|
*
|
|
* Output:
|
|
* e2e/scenarios/cross-module-data-consistency.json
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const SCENARIOS_DIR = path.resolve(__dirname, '..', 'scenarios');
|
|
|
|
const H = `const w=ms=>new Promise(r=>setTimeout(r,ms));`;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// STEP 1: 판매>거래처관리에서 거래처명 캡처
|
|
// ════════════════════════════════════════════════════════════════
|
|
const CAPTURE_VENDOR_FROM_SALES = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'CAPTURE_VENDOR'};`,
|
|
`await w(1500);`,
|
|
`const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`,
|
|
`R.rowCount=rows.length;`,
|
|
`if(rows.length===0){R.warn='판매>거래처관리 테이블에 데이터 없음';R.ok=true;return JSON.stringify(R);}`,
|
|
// 첫 행에서 거래처명 추출 (보통 2번째 또는 3번째 셀)
|
|
`const cells=rows[0].querySelectorAll('td');`,
|
|
`let vendorName='';`,
|
|
`for(let i=1;i<cells.length&&i<6;i++){`,
|
|
` const t=cells[i]?.innerText?.trim();`,
|
|
// 숫자만, 날짜, 체크박스 제외
|
|
` if(t&&t.length>=2&&t.length<=30&&!/^[\\d,.]+$/.test(t)&&!/^\\d{4}[-/]/.test(t)&&!cells[i].querySelector('input[type="checkbox"]')){`,
|
|
` vendorName=t;break;`,
|
|
` }`,
|
|
`}`,
|
|
`R.vendorName=vendorName;`,
|
|
`if(!vendorName){R.warn='거래처명 추출 실패';R.ok=true;return JSON.stringify(R);}`,
|
|
// 전역 저장
|
|
`if(!window.__CROSS_DATA__)window.__CROSS_DATA__={};`,
|
|
`window.__CROSS_DATA__.vendorName=vendorName;`,
|
|
`R.ok=true;`,
|
|
`return JSON.stringify(R);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// STEP 2: 회계>거래처관리에서 동일 거래처 확인
|
|
// ════════════════════════════════════════════════════════════════
|
|
const VERIFY_VENDOR_IN_ACCOUNTING = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'VERIFY_VENDOR_ACC'};`,
|
|
`await w(2000);`,
|
|
`const vendorName=window.__CROSS_DATA__?.vendorName;`,
|
|
`if(!vendorName){R.warn='캡처된 거래처명 없음 (이전 단계 실패)';R.ok=true;return JSON.stringify(R);}`,
|
|
`R.searchTarget=vendorName;`,
|
|
// 검색 입력란에 거래처명 입력
|
|
`const searchInput=document.querySelector('input[placeholder*="검색"]')||document.querySelector('input[type="search"]')||document.querySelector('input[role="searchbox"]');`,
|
|
`if(searchInput){`,
|
|
` searchInput.focus();await w(200);`,
|
|
` const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;`,
|
|
` if(nativeSetter)nativeSetter.call(searchInput,vendorName);else searchInput.value=vendorName;`,
|
|
` searchInput.dispatchEvent(new Event('input',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new Event('change',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));`,
|
|
` await w(2500);`,
|
|
` R.searchUsed=true;`,
|
|
`}else{R.searchUsed=false;}`,
|
|
// 테이블에서 거래처명 존재 확인
|
|
`const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`,
|
|
`R.rowCount=rows.length;`,
|
|
`const found=rows.some(r=>r.innerText?.includes(vendorName));`,
|
|
`R.vendorFound=found;`,
|
|
// 검색 안 했으면 전체 페이지 텍스트에서도 확인
|
|
`if(!found&&!searchInput){`,
|
|
` R.vendorFoundInPage=document.body.innerText.includes(vendorName);`,
|
|
`}`,
|
|
`if(!found){R.warn='⚠️ 회계>거래처관리에서 ['+vendorName+'] 미발견 - 모듈 간 데이터 불일치 가능';R.ok=true;}`,
|
|
`else{R.info='✅ 판매/회계 거래처 데이터 일치 확인: '+vendorName;R.ok=true;}`,
|
|
// 검색 초기화
|
|
`if(searchInput){`,
|
|
` const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;`,
|
|
` if(ns)ns.call(searchInput,'');else searchInput.value='';`,
|
|
` searchInput.dispatchEvent(new Event('input',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new Event('change',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));`,
|
|
` await w(1500);`,
|
|
`}`,
|
|
`return JSON.stringify(R);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// STEP 3: 판매>단가관리에서 품목명 캡처
|
|
// ════════════════════════════════════════════════════════════════
|
|
const CAPTURE_ITEM_FROM_SALES = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'CAPTURE_ITEM'};`,
|
|
`await w(1500);`,
|
|
`const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`,
|
|
`R.rowCount=rows.length;`,
|
|
`if(rows.length===0){R.warn='판매>단가관리 테이블에 데이터 없음';R.ok=true;return JSON.stringify(R);}`,
|
|
`const cells=rows[0].querySelectorAll('td');`,
|
|
`let itemName='';`,
|
|
`for(let i=1;i<cells.length&&i<6;i++){`,
|
|
` const t=cells[i]?.innerText?.trim();`,
|
|
` if(t&&t.length>=2&&t.length<=30&&!/^[\\d,.]+$/.test(t)&&!/^\\d{4}[-/]/.test(t)&&!cells[i].querySelector('input[type="checkbox"]')){`,
|
|
` itemName=t;break;`,
|
|
` }`,
|
|
`}`,
|
|
`R.itemName=itemName;`,
|
|
`if(!itemName){R.warn='품목명 추출 실패';R.ok=true;return JSON.stringify(R);}`,
|
|
`if(!window.__CROSS_DATA__)window.__CROSS_DATA__={};`,
|
|
`window.__CROSS_DATA__.itemName=itemName;`,
|
|
`R.ok=true;`,
|
|
`return JSON.stringify(R);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// STEP 4: 생산>품목관리에서 동일 품목 확인
|
|
// ════════════════════════════════════════════════════════════════
|
|
const VERIFY_ITEM_IN_PRODUCTION = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'VERIFY_ITEM_PROD'};`,
|
|
`await w(2000);`,
|
|
`const itemName=window.__CROSS_DATA__?.itemName;`,
|
|
`if(!itemName){R.warn='캡처된 품목명 없음 (이전 단계 실패)';R.ok=true;return JSON.stringify(R);}`,
|
|
`R.searchTarget=itemName;`,
|
|
// 검색
|
|
`const searchInput=document.querySelector('input[placeholder*="검색"]')||document.querySelector('input[type="search"]')||document.querySelector('input[role="searchbox"]');`,
|
|
`if(searchInput){`,
|
|
` searchInput.focus();await w(200);`,
|
|
` const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;`,
|
|
` if(nativeSetter)nativeSetter.call(searchInput,itemName);else searchInput.value=itemName;`,
|
|
` searchInput.dispatchEvent(new Event('input',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new Event('change',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));`,
|
|
` await w(2500);`,
|
|
` R.searchUsed=true;`,
|
|
`}else{R.searchUsed=false;}`,
|
|
// 테이블에서 품목명 존재 확인
|
|
`const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`,
|
|
`R.rowCount=rows.length;`,
|
|
`const found=rows.some(r=>r.innerText?.includes(itemName));`,
|
|
`R.itemFound=found;`,
|
|
`if(!found){R.warn='⚠️ 생산>품목관리에서 ['+itemName+'] 미발견 - 모듈 간 데이터 불일치 가능';R.ok=true;}`,
|
|
`else{R.info='✅ 판매/생산 품목 데이터 일치 확인: '+itemName;R.ok=true;}`,
|
|
// 검색 초기화
|
|
`if(searchInput){`,
|
|
` const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;`,
|
|
` if(ns)ns.call(searchInput,'');else searchInput.value='';`,
|
|
` searchInput.dispatchEvent(new Event('input',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new Event('change',{bubbles:true}));`,
|
|
` searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));`,
|
|
` await w(1500);`,
|
|
`}`,
|
|
`return JSON.stringify(R);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
function generateScenario() {
|
|
const steps = [];
|
|
let id = 1;
|
|
|
|
// ─── Part 1: 판매 거래처 → 회계 거래처 교차 확인 ───
|
|
steps.push({ id: id++, name: '[판매 > 거래처관리] 페이지 로드 대기', action: 'wait', timeout: 3000 });
|
|
steps.push({ id: id++, name: '[판매 > 거래처관리] 테이블 로드 대기', action: 'wait_for_table', timeout: 5000 });
|
|
steps.push({ id: id++, name: '[판매 > 거래처관리] 거래처명 캡처', action: 'evaluate', script: CAPTURE_VENDOR_FROM_SALES, timeout: 10000, phase: 'CAPTURE_VENDOR' });
|
|
|
|
// 회계>거래처관리로 이동
|
|
steps.push({ id: id++, name: '[회계 > 거래처관리] 메뉴 이동', action: 'menu_navigate', level1: '회계관리', level2: '거래처관리', timeout: 10000 });
|
|
steps.push({ id: id++, name: '[회계 > 거래처관리] 페이지 로드 대기', action: 'wait', timeout: 3000 });
|
|
steps.push({ id: id++, name: '[회계 > 거래처관리] 테이블 로드 대기', action: 'wait_for_table', timeout: 5000 });
|
|
steps.push({ id: id++, name: '[회계 > 거래처관리] 거래처 존재 확인', action: 'evaluate', script: VERIFY_VENDOR_IN_ACCOUNTING, timeout: 15000, phase: 'VERIFY_VENDOR_ACC' });
|
|
|
|
// ─── Part 2: 판매 단가 → 생산 품목 교차 확인 ───
|
|
steps.push({ id: id++, name: '[판매 > 단가관리] 메뉴 이동', action: 'menu_navigate', level1: '판매관리', level2: '단가관리', timeout: 10000 });
|
|
steps.push({ id: id++, name: '[판매 > 단가관리] 페이지 로드 대기', action: 'wait', timeout: 3000 });
|
|
steps.push({ id: id++, name: '[판매 > 단가관리] 테이블 로드 대기', action: 'wait_for_table', timeout: 5000 });
|
|
steps.push({ id: id++, name: '[판매 > 단가관리] 품목명 캡처', action: 'evaluate', script: CAPTURE_ITEM_FROM_SALES, timeout: 10000, phase: 'CAPTURE_ITEM' });
|
|
|
|
// 생산>품목관리로 이동
|
|
steps.push({ id: id++, name: '[생산 > 품목관리] 메뉴 이동', action: 'menu_navigate', level1: '생산관리', level2: '품목관리', timeout: 10000 });
|
|
steps.push({ id: id++, name: '[생산 > 품목관리] 페이지 로드 대기', action: 'wait', timeout: 3000 });
|
|
steps.push({ id: id++, name: '[생산 > 품목관리] 테이블 로드 대기', action: 'wait_for_table', timeout: 5000 });
|
|
steps.push({ id: id++, name: '[생산 > 품목관리] 품목 존재 확인', action: 'evaluate', script: VERIFY_ITEM_IN_PRODUCTION, timeout: 15000, phase: 'VERIFY_ITEM_PROD' });
|
|
|
|
return {
|
|
id: 'cross-module-data-consistency',
|
|
name: '모듈 간 데이터 일관성 검증 (판매↔회계, 판매↔생산)',
|
|
version: '1.0.0',
|
|
auth: { role: 'admin' },
|
|
menuNavigation: { level1: '판매관리', level2: '거래처관리' },
|
|
screenshotPolicy: { captureOnFail: true, captureOnPass: false },
|
|
steps,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
if (!fs.existsSync(SCENARIOS_DIR)) fs.mkdirSync(SCENARIOS_DIR, { recursive: true });
|
|
const scenario = generateScenario();
|
|
const fp = path.join(SCENARIOS_DIR, `${scenario.id}.json`);
|
|
fs.writeFileSync(fp, JSON.stringify(scenario, null, 2), 'utf-8');
|
|
console.log(` ${scenario.id}.json (${scenario.steps.length} steps)`);
|
|
console.log(`\n Generated 1 scenario\n Run: node e2e/runner/run-all.js --filter cross-module`);
|
|
}
|
|
|
|
main();
|