#!/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=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=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();