#!/usr/bin/env node /** * Business Workflow E2E Scenario Generator (Phase 3 Wave 1) * * Generates multi-module workflow scenarios that test real business journeys * spanning multiple modules. Each workflow chains operations across modules * using window.__WORKFLOW_CTX__ for data passing. * * Usage: node e2e/runner/gen-business-workflow.js * Output: e2e/scenarios/workflow-*.json (5 scenarios) */ 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));`; // ════════════════════════════════════════════════════════════════ // Shared script builders // ════════════════════════════════════════════════════════════════ function captureFirstRowCell(phase, varName, cellIndex = 1, fallbackCells = [2, 3]) { return [ `(async()=>{`, H, `const R={phase:'${phase}'};`, `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 testRow=rows.find(r=>r.innerText?.includes('E2E_TEST_'));`, `const targetRow=testRow||rows[0];`, `R.usedTestRow=!!testRow;`, `const cells=targetRow.querySelectorAll('td');`, `let val='';`, `const indices=[${cellIndex},${fallbackCells.join(',')}];`, `for(const i of indices){`, ` const t=cells[i]?.innerText?.trim();`, ` if(t&&t.length>=2&&t.length<=40&&!/^[\\d,.]+$/.test(t)&&!/^\\d{4}[-/]/.test(t)){val=t;break;}`, `}`, `R.${varName}=val;`, `if(!val){R.warn='${varName} 추출 실패';R.ok=true;return JSON.stringify(R);}`, `if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={};`, `window.__WORKFLOW_CTX__.${varName}=val;`, `R.ok=true;R.info='캐처: '+val;`, `return JSON.stringify(R);`, `})()`, ].join(''); } function verifyInModule(phase, varName, moduleName) { return [ `(async()=>{`, H, `const R={phase:'${phase}'};`, `await w(2000);`, `const val=window.__WORKFLOW_CTX__?.${varName};`, `if(!val){R.warn='컨텍스트에 ${varName} 없음';R.ok=true;return JSON.stringify(R);}`, `R.searchTarget=val;`, `const si=document.querySelector('input[placeholder*="검색"]')||document.querySelector('input[type="search"]');`, `if(si){`, ` si.focus();await w(200);`, ` const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;`, ` if(ns)ns.call(si,val);else si.value=val;`, ` si.dispatchEvent(new Event('input',{bubbles:true}));`, ` si.dispatchEvent(new Event('change',{bubbles:true}));`, ` await w(2500);`, `}`, `const found=document.body.innerText.includes(val);`, `R.found=found;`, `if(found){R.info='✅ ${moduleName}에서 ['+val+'] 확인';R.ok=true;}`, `else{R.warn='⚠️ ${moduleName}에서 ['+val+'] 미발견';R.ok=false;}`, // Clear search `if(si){`, ` const ns2=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;`, ` if(ns2)ns2.call(si,'');else si.value='';`, ` si.dispatchEvent(new Event('input',{bubbles:true}));await w(1000);`, `}`, `return JSON.stringify(R);`, `})()`, ].join(''); } function countTableRows(phase) { return [ `(async()=>{`, H, `const R={phase:'${phase}'};`, `await w(1500);`, `const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`, `R.rowCount=rows.length;`, `R.ok=true;`, `R.info='테이블 행: '+rows.length;`, `return JSON.stringify(R);`, `})()`, ].join(''); } // ════════════════════════════════════════════════════════════════ // Workflow Definitions // ════════════════════════════════════════════════════════════════ const WORKFLOWS = [ // ── 1. Purchase-to-Payment ── { id: 'workflow-purchase-to-payment', name: '비즈니스 워크플로우: 구매→매입 흐름', startMenu: { level1: '판매관리', level2: '거래처관리' }, modules: [ { label: '판매 > 거래처관리', menu: null, // start page steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: captureFirstRowCell('CAPTURE_VENDOR', 'vendorName'), phase: 'CAPTURE_VENDOR' }, ], }, { label: '회계 > 거래처관리', menu: { level1: '회계관리', level2: '거래처관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: verifyInModule('VERIFY_VENDOR_ACC', 'vendorName', '회계>거래처관리'), phase: 'VERIFY_VENDOR_ACC' }, ], }, ], }, // ── 2. Employee Onboarding ── { id: 'workflow-employee-onboarding', name: '비즈니스 워크플로우: 사원등록→부서→근태→급여 흐름', startMenu: { level1: '인사관리', level2: '사원관리' }, modules: [ { label: '인사 > 사원관리', menu: null, // start page steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: captureFirstRowCell('CAPTURE_EMPLOYEE', 'employeeName'), phase: 'CAPTURE_EMPLOYEE' }, ], }, { label: '인사 > 부서관리', menu: { level1: '인사관리', level2: '부서관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_DEPARTMENTS'), phase: 'CHECK_DEPARTMENTS' }, ], }, { label: '인사 > 근태관리', menu: { level1: '인사관리', level2: '근태관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'evaluate', script: verifyInModule('VERIFY_EMPLOYEE_ATTEND', 'employeeName', '근태관리'), phase: 'VERIFY_EMPLOYEE_ATTEND' }, ], }, { label: '인사 > 급여관리', menu: { level1: '인사관리', level2: '급여관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: verifyInModule('VERIFY_EMPLOYEE_SALARY', 'employeeName', '급여관리'), phase: 'VERIFY_EMPLOYEE_SALARY' }, ], }, ], }, // ── 3. Sales Order Lifecycle ── { id: 'workflow-sales-lifecycle', name: '비즈니스 워크플로우: 거래처→단가→수주→매출 흐름', startMenu: { level1: '판매관리', level2: '거래처관리' }, modules: [ { label: '판매 > 거래처관리', menu: null, // start page steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: captureFirstRowCell('CAPTURE_CLIENT', 'clientName'), phase: 'CAPTURE_CLIENT' }, ], }, { label: '판매 > 단가관리', menu: { level1: '판매관리', level2: '단가관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: captureFirstRowCell('CAPTURE_PRICE_ITEM', 'itemName', 1, [2, 3]), phase: 'CAPTURE_PRICE_ITEM' }, ], }, { label: '판매 > 수주관리', menu: { level1: '판매관리', level2: '수주관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_ORDERS'), phase: 'CHECK_ORDERS' }, ], }, { label: '회계 > 매출관리', menu: { level1: '회계관리', level2: '매출관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_SALES'), phase: 'CHECK_SALES' }, ], }, ], }, // ── 4. Board Approval Flow ── { id: 'workflow-board-approval', name: '비즈니스 워크플로우: 게시판→결재기안→결재함 흐름', startMenu: { level1: '게시판', level2: '자유게시판' }, modules: [ { label: '게시판 > 자유게시판', menu: null, // start page steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: captureFirstRowCell('CAPTURE_POST', 'postTitle'), phase: 'CAPTURE_POST' }, ], }, { label: '결재관리 > 기안함', menu: { level1: '결재관리', level2: '기안함' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_DRAFTS'), phase: 'CHECK_DRAFTS' }, ], }, { label: '결재관리 > 결재함', menu: { level1: '결재관리', level2: '결재함' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_APPROVALS'), phase: 'CHECK_APPROVALS' }, ], }, { label: '결재관리 > 참조함', menu: { level1: '결재관리', level2: '참조함' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_REFERENCES'), phase: 'CHECK_REFERENCES' }, ], }, ], }, // ── 5. Inventory Cycle ── { id: 'workflow-inventory-cycle', name: '비즈니스 워크플로우: 품목→입고→재고→출고 흐름', startMenu: { level1: '생산관리', level2: '품목관리' }, modules: [ { label: '생산 > 품목관리', menu: null, // start page steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 10000 }, { action: 'evaluate', script: captureFirstRowCell('CAPTURE_ITEM', 'itemName'), phase: 'CAPTURE_ITEM' }, ], }, { label: '자재관리 > 입고관리', menu: { level1: '자재관리', level2: '입고관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: verifyInModule('VERIFY_ITEM_RECEIVING', 'itemName', '입고관리'), phase: 'VERIFY_ITEM_RECEIVING' }, ], }, { label: '자재관리 > 재고현황', menu: { level1: '자재관리', level2: '재고현황' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: verifyInModule('VERIFY_ITEM_STOCK', 'itemName', '재고현황'), phase: 'VERIFY_ITEM_STOCK' }, ], }, { label: '회계 > 출금관리', menu: { level1: '회계관리', level2: '출금관리' }, steps: [ { action: 'wait', timeout: 3000 }, { action: 'wait_for_table', timeout: 5000 }, { action: 'evaluate', script: countTableRows('CHECK_WITHDRAWAL'), phase: 'CHECK_WITHDRAWAL' }, ], }, ], }, ]; // ════════════════════════════════════════════════════════════════ // Generate scenario JSON from workflow definition // ════════════════════════════════════════════════════════════════ function generateScenario(workflow) { const steps = []; let id = 1; for (const mod of workflow.modules) { // Menu navigation if needed if (mod.menu) { steps.push({ id: id++, name: `[${mod.label}] 메뉴 이동`, action: 'menu_navigate', level1: mod.menu.level1, level2: mod.menu.level2, timeout: 10000, }); } // Module steps for (const step of mod.steps) { steps.push({ id: id++, name: `[${mod.label}] ${step.phase || step.action}`, ...step, }); } } return { id: workflow.id, name: workflow.name, version: '1.0.0', category: 'workflow', auth: { role: 'admin' }, menuNavigation: workflow.startMenu, screenshotPolicy: { captureOnFail: true, captureOnPass: false }, steps, }; } function main() { if (!fs.existsSync(SCENARIOS_DIR)) fs.mkdirSync(SCENARIOS_DIR, { recursive: true }); console.log('Generating Business Workflow scenarios..\n'); let total = 0; for (const workflow of WORKFLOWS) { const scenario = generateScenario(workflow); 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)`); total++; } console.log(`\n Generated ${total} workflow scenarios`); console.log(` Run: node e2e/runner/run-all.js --filter workflow`); } main();