Files
sam-hotfix/e2e/runner/gen-business-workflow.js
김보곤 48eba1e716 refactor: E2E 시나리오 생성기 8종 품질 개선 (false positive 제거 + flaky 패턴 수정)
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>
2026-02-19 21:54:57 +09:00

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();