Phase 1: R.ok=true 무조건 반환 → 조건부 검증으로 교체 (36개 시나리오 영향) - gen-edge-cases.js: R.ok=R.validationTriggered, R.ok=R.allConsistent 등 - gen-pagination-sort.js: R.ok=R.sortWorked!==false - gen-search-function.js: R.ok=R.searchWorked!==false - gen-form-validation.js: R.ok=R.validationTriggered||R.hasValidation - gen-batch-create.js: R.ok=R.created!==false - gen-reload-persist.js: R.ok=R.persisted!==false - gen-detail-roundtrip.js: R.ok=R.matched!==false - gen-business-workflow.js: R.ok=!R.error&&R.phaseCompleted!==false Phase 2: rows[0] 맹목적 접근 → E2E_TEST_ 스마트 타겟팅 추가 - gen-detail-roundtrip.js, gen-business-workflow.js에 testRow 탐색 패턴 적용 결과: 184 시나리오 중 9개 정당한 FAIL 노출 (실제 버그 5건 발견) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
371 lines
14 KiB
JavaScript
371 lines
14 KiB
JavaScript
#!/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();
|