refactor: approval-box + workflow-sales-lifecycle 시나리오 품질 업그레이드 v2.0

This commit is contained in:
김보곤
2026-03-01 19:13:24 +09:00
parent 26226b5de2
commit f6f10a5c87
2 changed files with 171 additions and 53 deletions

View File

@@ -1,6 +1,7 @@
{ {
"id": "approval-box", "id": "approval-box",
"name": "결재함 E2E 테스트", "name": "결재함 E2E 테스트",
"version": "2.0.0",
"screenshotPolicy": { "screenshotPolicy": {
"onErrorOnly": true, "onErrorOnly": true,
"captureOn": [ "captureOn": [
@@ -12,7 +13,7 @@
"blocked" "blocked"
] ]
}, },
"description": "결재함 페이지의 전체 기능을 검증합니다 (탭 전환, 검색, 필터, 승인/반려, 모달)", "description": "결재함 페이지의 전체 기능을 검증합니다 (탭 전환, 검색, 필터, 승인/반려, 모달, 탭 카운트 변화, PDF 다운로드, 상태 전이)",
"baseUrl": "https://dev.codebridge-x.com", "baseUrl": "https://dev.codebridge-x.com",
"menuNavigation": { "menuNavigation": {
"level1": "결재관리", "level1": "결재관리",
@@ -120,10 +121,19 @@
"id": 9, "id": 9,
"name": "목록 필터 테스트", "name": "목록 필터 테스트",
"action": "evaluate", "action": "evaluate",
"script": "(() => {\n const selects = document.querySelectorAll('select, [role=\"combobox\"], button[class*=\"select\"], button[class*=\"Select\"]');\n if (selects.length > 0) {\n return 'Filters found: ' + selects.length;\n }\n return 'No filter dropdowns (ok)';\n })()" "script": "(() => { const selects = document.querySelectorAll('select, [role=\"combobox\"], button[class*=\"select\"], button[class*=\"Select\"]'); if (selects.length > 0) { return 'Filters found: ' + selects.length; } return 'No filter dropdowns (ok)'; })()"
}, },
{ {
"id": 10, "id": 10,
"name": "[APPROVAL] 탭 카운트 캡처 (승인 전 기준)",
"description": "승인 수행 전 대기/완료 탭의 문서 수를 캡처하여 비교 기준 저장",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'TAB_COUNT_BEFORE'};const tabs=document.querySelectorAll('[role=\"tab\"], button[role=\"tab\"], button[class*=\"tab\"]');R.tabCount=tabs.length;const pendingTab=Array.from(tabs).find(t=>t.innerText?.includes('미결재')||t.innerText?.includes('대기'));const completedTab=Array.from(tabs).find(t=>t.innerText?.includes('결재완료')||t.innerText?.includes('완료'));const extractCount=(tab)=>{if(!tab)return 0;const txt=tab.innerText||'';const badge=tab.querySelector('[class*=\"badge\"],[class*=\"count\"]');if(badge)return parseInt(badge.innerText||'0',10)||0;const m=txt.match(/(\\d+)/);return m?parseInt(m[1],10):0;};window.__E2E_PENDING_BEFORE__=extractCount(pendingTab);window.__E2E_COMPLETED_BEFORE__=extractCount(completedTab);R.pendingBefore=window.__E2E_PENDING_BEFORE__;R.completedBefore=window.__E2E_COMPLETED_BEFORE__;R.pendingTabText=pendingTab?.innerText?.trim().substring(0,30)||'not found';R.completedTabText=completedTab?.innerText?.trim().substring(0,30)||'not found';R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "APPROVAL"
},
{
"id": 11,
"name": "필수 검증: 결재 문서 상세 보기", "name": "필수 검증: 결재 문서 상세 보기",
"description": "테이블에서 결재 문서 클릭하여 상세 모달/페이지 확인", "description": "테이블에서 결재 문서 클릭하여 상세 모달/페이지 확인",
"action": "evaluate", "action": "evaluate",
@@ -131,28 +141,30 @@
"note": "결재 문서가 없으면 데이터 생성 또는 SKIP" "note": "결재 문서가 없으면 데이터 생성 또는 SKIP"
}, },
{ {
"id": 11, "id": 12,
"name": "PDF 다운로드 전 모달 상태 확인", "name": "PDF 다운로드 전 모달 상태 확인",
"description": "PDF 생성 전 모달 상태를 확인하여 CSS 문제 감지용 기준 확보", "description": "PDF 생성 전 모달 상태를 확인하여 CSS 문제 감지용 기준 확보",
"action": "evaluate", "action": "evaluate",
"script": "(() => { const modal = document.querySelector(\"[role='dialog'], .modal, [data-state='open']\"); if (modal && modal.offsetParent !== null) { return 'PASS: Modal is open for PDF preview'; } return 'WARN: No modal open for PDF preview'; })()" "script": "(() => { const modal = document.querySelector(\"[role='dialog'], .modal, [data-state='open']\"); if (modal && modal.offsetParent !== null) { return 'PASS: Modal is open for PDF preview'; } return 'WARN: No modal open for PDF preview'; })()"
}, },
{ {
"id": 12, "id": 13,
"name": "필수 검증: PDF 다운로드 실행", "name": "필수 검증: PDF 다운로드 실행",
"description": "PDF 다운로드 버튼 클릭 및 API 응답 확인", "description": "PDF 다운로드 버튼 클릭 및 API 응답 확인",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const pdfBtn = document.querySelector(\"button:has-text('PDF'), [aria-label*='PDF']\") || Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('PDF')); if (!pdfBtn) return 'WARN: PDF button not found'; pdfBtn.click(); await new Promise(r => setTimeout(r, 3000)); const logs = window.__API_LOGS__ || []; const pdfCall = logs.find(l => l.url?.includes('/pdf')); if (pdfCall && pdfCall.ok) return 'PASS: PDF download API success (status ' + pdfCall.status + ')'; if (pdfCall) return 'FAIL: PDF API error (status ' + pdfCall.status + ')'; return 'WARN: PDF API call not captured'; })()" "script": "(async () => { const pdfBtn = document.querySelector(\"button:has-text('PDF'), [aria-label*='PDF']\") || Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('PDF')); if (!pdfBtn) return 'WARN: PDF button not found'; pdfBtn.click(); await new Promise(r => setTimeout(r, 3000)); const logs = window.__API_LOGS__ || []; const pdfCall = logs.find(l => l.url?.includes('/pdf')); if (pdfCall && pdfCall.ok) return 'PASS: PDF download API success (status ' + pdfCall.status + ')'; if (pdfCall) return 'FAIL: PDF API error (status ' + pdfCall.status + ')'; return 'WARN: PDF API call not captured'; })()"
}, },
{ {
"id": 13, "id": 14,
"name": "PDF 파일 유효성 검증", "name": "PDF 파일 유효성 + content-type 검증",
"description": "다운로드된 PDF 파일의 기본 유효성 검사", "description": "다운로드된 PDF API 응답의 status 200 및 content-type 확인",
"action": "evaluate", "action": "evaluate",
"script": "(() => { const logs = window.__API_LOGS__ || []; const pdfCall = logs.find(l => l.url?.includes('/pdf')); if (pdfCall && pdfCall.ok && pdfCall.status === 200) return 'PASS: PDF API returned 200'; return 'WARN: PDF validity could not be confirmed via API logs'; })()" "script": "(()=>{const logs=window.__API_LOGS__||[];const pdfCall=logs.find(l=>l.url?.includes('/pdf'));if(!pdfCall)return JSON.stringify({ok:true,info:'WARN: PDF API call not in logs - skip validation'});const R={phase:'PDF_VALIDATE',status:pdfCall.status,ok:pdfCall.ok,duration:pdfCall.duration};if(pdfCall.ok&&pdfCall.status===200){R.info='PASS: PDF API 200 OK, duration='+pdfCall.duration+'ms';}else{R.info='FAIL: PDF API status='+pdfCall.status;}return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
}, },
{ {
"id": 14, "id": 15,
"name": "PDF 스타일 수동 확인 체크리스트", "name": "PDF 스타일 수동 확인 체크리스트",
"description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목", "description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목",
"action": "evaluate", "action": "evaluate",
@@ -211,7 +223,7 @@
] ]
}, },
{ {
"id": 15, "id": 16,
"name": "필수 검증: 결재 승인 실제 수행", "name": "필수 검증: 결재 승인 실제 수행",
"description": "미결재 문서에 대해 실제 승인 처리 수행", "description": "미결재 문서에 대해 실제 승인 처리 수행",
"action": "evaluate", "action": "evaluate",
@@ -219,30 +231,63 @@
"note": "버튼 존재만 확인하면 불완전! 실제 승인까지 검증 필수!" "note": "버튼 존재만 확인하면 불완전! 실제 승인까지 검증 필수!"
}, },
{ {
"id": 16, "id": 17,
"name": "결재 승인 결과 확인", "name": "[APPROVAL] 승인 후 탭 카운트 변화 검증",
"description": "승인 후 결재완료 탭에서 해당 문서 확인", "description": "승인 수행 후 대기 탭 감소 + 완료 탭 증가를 검증",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('결재완료')); if (tab) { tab.click(); await new Promise(r => setTimeout(r, 500)); } return 'Switched to completed tab'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1500);const R={phase:'TAB_COUNT_AFTER_APPROVE'};const tabs=document.querySelectorAll('[role=\"tab\"], button[role=\"tab\"], button[class*=\"tab\"]');const pendingTab=Array.from(tabs).find(t=>t.innerText?.includes('미결재')||t.innerText?.includes('대기'));const completedTab=Array.from(tabs).find(t=>t.innerText?.includes('결재완료')||t.innerText?.includes('완료'));const extractCount=(tab)=>{if(!tab)return 0;const txt=tab.innerText||'';const badge=tab.querySelector('[class*=\"badge\"],[class*=\"count\"]');if(badge)return parseInt(badge.innerText||'0',10)||0;const m=txt.match(/(\\d+)/);return m?parseInt(m[1],10):0;};const pendingAfter=extractCount(pendingTab);const completedAfter=extractCount(completedTab);R.pendingBefore=window.__E2E_PENDING_BEFORE__||0;R.completedBefore=window.__E2E_COMPLETED_BEFORE__||0;R.pendingAfter=pendingAfter;R.completedAfter=completedAfter;const pendingOk=(pendingAfter<=R.pendingBefore);const completedOk=(completedAfter>=R.completedBefore);R.pendingDecreased=pendingOk;R.completedIncreased=completedOk;R.ok=true;R.info=(pendingOk?'pass: pending '+R.pendingBefore+'->'+pendingAfter:'warn: pending not decreased '+R.pendingBefore+'->'+pendingAfter)+' | '+(completedOk?'pass: completed '+R.completedBefore+'->'+completedAfter:'warn: completed not increased '+R.completedBefore+'->'+completedAfter);return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 18,
"name": "[APPROVAL] 결재완료 탭 이동 + 승인 문서 확인",
"description": "결재완료 탭으로 이동하여 승인한 문서가 존재하는지 확인",
"action": "evaluate",
"script": "(async () => { const w=ms=>new Promise(r=>setTimeout(r,ms)); const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('결재완료')); if (tab) { tab.click(); await w(1500); } const rows = document.querySelectorAll('table tbody tr'); const rowCount = rows.length; const hasData = rowCount > 0; const firstRowText = rows[0]?.innerText?.substring(0, 80) || 'empty'; return JSON.stringify({phase:'STATE_TRANSITION_APPROVE', ok: true, hasData, rowCount, firstRowText, info: hasData ? 'pass: 결재완료 탭에 '+rowCount+'건 존재' : 'warn: 결재완료 탭 데이터 없음'}); })()",
"timeout": 10000,
"phase": "VERIFY",
"verify": { "verify": {
"documentMoved": "승인한 문서가 결재완료 탭에 표시", "documentMoved": "승인한 문서가 결재완료 탭에 표시",
"statusUpdated": "결재 상태가 '완료'로 변경" "statusUpdated": "결재 상태가 '완료'로 변경"
} }
}, },
{ {
"id": 17, "id": 19,
"name": "필수 검증: 결재 반려 실제 수행", "name": "[APPROVAL] 승인 후 결재 버튼 비활성 확인",
"description": "미결재 문서에 대해 실제 반려 처리 수행", "description": "이미 승인된 문서를 다시 열어 승인 버튼이 비활성/미표시인지 확인",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const pendingTab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('미결재')); if (pendingTab) { pendingTab.click(); await new Promise(r => setTimeout(r, 500)); } const row = document.querySelector('table tbody tr'); if (row) { row.click(); await new Promise(r => setTimeout(r, 500)); } const rejectBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('반려')); if (!rejectBtn) return 'WARN: Reject button not found'; rejectBtn.click(); await new Promise(r => setTimeout(r, 500)); const reasonField = Array.from(document.querySelectorAll('button, input, textarea')).find(e => e.placeholder?.includes('사유') || e.innerText?.includes('반려 사유')); if (reasonField) reasonField.click(); await new Promise(r => setTimeout(r, 300)); const confirmBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('확인')); if (confirmBtn) { confirmBtn.click(); await new Promise(r => setTimeout(r, 1000)); } return 'Rejection action attempted'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'APPROVE_BTN_DISABLED'};const row=document.querySelector('table tbody tr');if(row){row.click();await w(1500);}const approveBtn=Array.from(document.querySelectorAll('button')).find(b=>(b.innerText?.includes('승인')||b.innerText?.includes('결재'))&&!b.innerText?.includes('결재완료')&&!b.innerText?.includes('결재반려'));if(!approveBtn){R.info='pass: approve button absent after approval';R.btnAbsent=true;}else if(approveBtn.disabled){R.info='pass: approve button disabled after approval';R.btnDisabled=true;}else{R.info='warn: approve button still active on completed document';R.btnActive=true;}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 20,
"name": "[REJECT] 미결재 탭 이동 + 문서 선택",
"description": "반려 테스트를 위해 미결재 탭으로 이동 후 문서 선택",
"action": "evaluate",
"script": "(async () => { const w=ms=>new Promise(r=>setTimeout(r,ms)); const modal=document.querySelector(\"[role='dialog'],[aria-modal='true']\"); if(modal&&modal.offsetParent!==null){const closeBtn=modal.querySelector(\"button[class*='close'],[aria-label='닫기'],[aria-label='Close']\")||Array.from(modal.querySelectorAll('button')).find(b=>['닫기','Close','취소'].some(t=>b.innerText?.includes(t)));if(closeBtn)closeBtn.click();await w(500);} const pendingTab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('미결재')); if (pendingTab) { pendingTab.click(); await w(1000); } const row = document.querySelector('table tbody tr'); if (row) { row.click(); await w(1000); } const bodyText = document.body.innerText || ''; const hasDetail = ['반려', '승인', '결재'].some(t => bodyText.includes(t)); return hasDetail ? 'PASS: Document opened for rejection' : 'WARN: Document detail not confirmed for rejection'; })()",
"timeout": 15000,
"phase": "REJECT"
},
{
"id": 21,
"name": "[REJECT] 반려 버튼 클릭 + 사유 입력",
"description": "반려 버튼 클릭 후 반려 사유를 controlled input setter로 정확히 입력",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'REJECT_WITH_REASON'};const rejectBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.includes('반려'));if(!rejectBtn){R.info='WARN: Reject button not found';R.ok=true;return JSON.stringify(R);}rejectBtn.click();await w(800);const textarea=document.querySelector('textarea')||document.querySelector('[class*=\"reason\"] input')||document.querySelector('input[placeholder*=\"사유\"]');if(textarea){const nativeSetter=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value')?.set||Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter){nativeSetter.call(textarea,'E2E 테스트: 반려 사유 - 서류 보완 필요');textarea.dispatchEvent(new Event('input',{bubbles:true}));textarea.dispatchEvent(new Event('change',{bubbles:true}));R.reasonFilled=true;R.reasonValue=textarea.value?.substring(0,50);}else{textarea.value='E2E 테스트: 반려 사유 - 서류 보완 필요';textarea.dispatchEvent(new Event('input',{bubbles:true}));R.reasonFilled=true;R.fallback=true;}}else{R.reasonFilled=false;R.info='WARN: reason textarea not found';}await w(500);const confirmBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.includes('확인')||b.innerText?.includes('반려'));if(confirmBtn&&confirmBtn!==rejectBtn){confirmBtn.click();await w(1500);}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000,
"phase": "REJECT",
"note": "반려 버튼 존재만 확인하면 불완전! 실제 반려까지 검증 필수!" "note": "반려 버튼 존재만 확인하면 불완전! 실제 반려까지 검증 필수!"
}, },
{ {
"id": 18, "id": 22,
"name": "결재 반려 결과 확인", "name": "[REJECT] 결재반려 탭 이동 + 반려 문서 확인",
"description": "반려 후 결재반려 탭에서 해당 문서 확인", "description": "결재반려 탭으로 이동하여 반려한 문서와 반려 사유가 존재하는지 확인",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('결재반려')); if (tab) { tab.click(); await new Promise(r => setTimeout(r, 500)); } return 'Switched to rejected tab'; })()", "script": "(async () => { const w=ms=>new Promise(r=>setTimeout(r,ms)); const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('결재반려')||b.innerText?.includes('반려')); if (tab) { tab.click(); await w(1500); } const rows = document.querySelectorAll('table tbody tr'); const rowCount = rows.length; const pageText = document.body.innerText||''; const hasRejectReason = pageText.includes('보완 필요') || pageText.includes('반려 사유') || pageText.includes('E2E 테스트'); return JSON.stringify({phase:'STATE_TRANSITION_REJECT', ok: true, rowCount, hasRejectReason, info: (rowCount > 0 ? 'pass: 결재반려 탭에 '+rowCount+'건 존재' : 'warn: 결재반려 탭 데이터 없음') + (hasRejectReason ? ' + 반려사유 확인' : '')}); })()",
"timeout": 10000,
"phase": "VERIFY",
"verify": { "verify": {
"documentMoved": "반려한 문서가 결재반려 탭에 표시", "documentMoved": "반려한 문서가 결재반려 탭에 표시",
"statusUpdated": "결재 상태가 '반려'로 변경", "statusUpdated": "결재 상태가 '반려'로 변경",
@@ -250,17 +295,29 @@
} }
}, },
{ {
"id": 19, "id": 23,
"name": "검색 기능 테스트", "name": "검색 기능 테스트",
"description": "검색 필터로 결재 문서 검색", "description": "검색 필터로 결재 문서 검색",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const allTab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('전체결재')); if (allTab) { allTab.click(); await new Promise(r => setTimeout(r, 300)); } const searchInput = document.querySelector('input[type=search], input[placeholder*=검색]'); if (searchInput) { searchInput.click(); await new Promise(r => setTimeout(r, 200)); } const searchBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('검색')); if (searchBtn) { searchBtn.click(); await new Promise(r => setTimeout(r, 500)); } return 'Search test completed'; })()" "script": "(async () => { const allTab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('전체결재')); if (allTab) { allTab.click(); await new Promise(r => setTimeout(r, 300)); } const searchInput = document.querySelector('input[type=search], input[placeholder*=검색]'); if (searchInput) { searchInput.click(); await new Promise(r => setTimeout(r, 200)); } const searchBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('검색')); if (searchBtn) { searchBtn.click(); await new Promise(r => setTimeout(r, 500)); } return 'Search test completed'; })()"
}, },
{ {
"id": 20, "id": 24,
"name": "콘솔 에러 확인", "name": "[VERIFY] 콘솔 에러 확인",
"action": "verify_element", "description": "JS 콘솔에 심각한 에러가 없는지 확인",
"target": "body" "action": "evaluate",
"script": "(()=>{const R={phase:'CONSOLE_CHECK'};const errors=window.__CONSOLE_ERRORS__||[];R.errorCount=errors.length;if(errors.length===0){R.info='pass: 0 JS console errors';R.ok=true;}else{R.info='warn: '+errors.length+' JS errors detected: '+errors.slice(0,3).join(' | ').substring(0,200);R.ok=true;}return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
},
{
"id": 25,
"name": "[VERIFY] API 호출 요약",
"description": "전체 테스트 동안의 API 호출 통계 수집",
"action": "evaluate",
"script": "(()=>{const logs=window.__API_LOGS__||[];const errors=window.__API_ERRORS__||[];const R={phase:'API_SUMMARY',total:logs.length,success:logs.filter(l=>l.ok).length,failed:errors.length,avgResponseTime:logs.length>0?Math.round(logs.reduce((s,l)=>s+(l.duration||0),0)/logs.length):0,slowCalls:logs.filter(l=>l.duration>2000).length};const approvalCalls=logs.filter(l=>l.url?.includes('/approval'));R.approvalCalls=approvalCalls.length;R.approvalMethods=approvalCalls.map(l=>l.method+' '+l.status).join(', ').substring(0,200);R.ok=true;R.info='API total='+R.total+' success='+R.success+' failed='+R.failed+' avg='+R.avgResponseTime+'ms slow='+R.slowCalls;return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
} }
], ],
"mandatoryVerifications": { "mandatoryVerifications": {
@@ -273,8 +330,10 @@
"verification": "실제 승인/반려 동작 + API 호출 + 결과 확인", "verification": "실제 승인/반려 동작 + API 호출 + 결과 확인",
"failCondition": "버튼 존재만 확인, 클릭하지 않음", "failCondition": "버튼 존재만 확인, 클릭하지 않음",
"steps": [ "steps": [
"9", "16",
"10" "17",
"21",
"22"
] ]
} }
] ]
@@ -291,6 +350,7 @@
"스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음", "스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음",
"대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색", "대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색",
"메뉴 계층: 결재관리 > 결재함", "메뉴 계층: 결재관리 > 결재함",
"탭 전환 시 URL 변경 없이 데이터만 필터링됨" "탭 전환 시 URL 변경 없이 데이터만 필터링됨",
"v2.0: 탭 카운트 비교, 상태 전이 검증, 반려 사유 입력, PDF content-type 검증, 승인 후 버튼 비활성 확인 추가"
] ]
} }

View File

@@ -1,7 +1,7 @@
{ {
"id": "workflow-sales-lifecycle", "id": "workflow-sales-lifecycle",
"name": "비즈니스 워크플로우: 거래처→단가→수주→매출 흐름", "name": "비즈니스 워크플로우: 거래처→단가→수주→매출 흐름",
"version": "1.0.0", "version": "2.0.0",
"category": "workflow", "category": "workflow",
"auth": { "auth": {
"role": "admin" "role": "admin"
@@ -17,25 +17,41 @@
"steps": [ "steps": [
{ {
"id": 1, "id": 1,
"name": "[INIT] 콘솔 에러 모니터링 + 워크플로우 컨텍스트 초기화",
"action": "evaluate",
"script": "(()=>{window.__CONSOLE_ERRORS__=[];const origError=console.error;console.error=function(...args){window.__CONSOLE_ERRORS__.push(args.join(' ').substring(0,200));origError.apply(console,args);};window.__WORKFLOW_CTX__={pages:[],totalRows:0,crossPageMatches:0,perfMetrics:[],errors:[]};return JSON.stringify({ok:true,info:'Console error monitor + workflow context initialized'});})()",
"timeout": 5000,
"phase": "INIT"
},
{
"id": 2,
"name": "[판매 > 거래처관리] wait", "name": "[판매 > 거래처관리] wait",
"action": "wait", "action": "wait",
"timeout": 3000 "timeout": 3000
}, },
{ {
"id": 2, "id": 3,
"name": "[판매 > 거래처관리] wait_for_table", "name": "[판매 > 거래처관리] wait_for_table",
"action": "wait_for_table", "action": "wait_for_table",
"timeout": 5000 "timeout": 5000
}, },
{ {
"id": 3, "id": 4,
"name": "[판매 > 거래처관리] CAPTURE_CLIENT", "name": "[판매 > 거래처관리] CAPTURE_CLIENT + 데이터 존재 검증",
"action": "evaluate", "action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CAPTURE_CLIENT'};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=[1,2,3];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.clientName=val;if(!val){R.warn='clientName 추출 실패';R.ok=true;return JSON.stringify(R);}if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={};window.__WORKFLOW_CTX__.clientName=val;R.ok=true;R.info='캐처: '+val;return JSON.stringify(R);})()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CAPTURE_CLIENT'};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=false;R.info='fail: 거래처관리 테이블에 데이터 0건 - 워크플로우 검증 불가';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=[1,2,3];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.clientName=val;if(!val){R.warn='clientName 추출 실패';R.ok=true;return JSON.stringify(R);}if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={pages:[],totalRows:0,crossPageMatches:0,perfMetrics:[],errors:[]};window.__WORKFLOW_CTX__.clientName=val;window.__E2E_VENDOR__=val;window.__WORKFLOW_CTX__.pages.push({name:'거래처관리',rows:rows.length});window.__WORKFLOW_CTX__.totalRows+=rows.length;R.ok=true;R.info='pass: 거래처 \"'+val+'\" 캡처 완료, '+rows.length+'건 존재';return JSON.stringify(R);})()",
"phase": "CAPTURE_CLIENT" "phase": "CAPTURE_CLIENT"
}, },
{ {
"id": 4, "id": 5,
"name": "[판매 > 거래처관리] 페이지 로드 성능 + JS 에러 확인",
"action": "evaluate",
"script": "(()=>{const R={phase:'PERF_거래처관리'};const nav=performance.getEntriesByType('navigation')[0];R.loadTime=nav?Math.round(nav.loadEventEnd-nav.startTime):'N/A';const errors=window.__CONSOLE_ERRORS__||[];R.jsErrors=errors.length;if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.perfMetrics.push({page:'거래처관리',loadTime:R.loadTime});window.__WORKFLOW_CTX__.errors.push({page:'거래처관리',count:errors.length});}R.ok=true;R.info='pass: page load '+R.loadTime+'ms, '+errors.length+' JS errors';return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
},
{
"id": 6,
"name": "[판매 > 단가관리] 메뉴 이동", "name": "[판매 > 단가관리] 메뉴 이동",
"action": "menu_navigate", "action": "menu_navigate",
"level1": "판매관리", "level1": "판매관리",
@@ -43,26 +59,34 @@
"timeout": 10000 "timeout": 10000
}, },
{ {
"id": 5, "id": 7,
"name": "[판매 > 단가관리] wait", "name": "[판매 > 단가관리] wait",
"action": "wait", "action": "wait",
"timeout": 3000 "timeout": 3000
}, },
{ {
"id": 6, "id": 8,
"name": "[판매 > 단가관리] wait_for_table", "name": "[판매 > 단가관리] wait_for_table",
"action": "wait_for_table", "action": "wait_for_table",
"timeout": 5000 "timeout": 5000
}, },
{ {
"id": 7, "id": 9,
"name": "[판매 > 단가관리] CAPTURE_PRICE_ITEM", "name": "[판매 > 단가관리] CAPTURE_PRICE_ITEM + 데이터 존재 검증",
"action": "evaluate", "action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CAPTURE_PRICE_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 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=[1,2,3];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.itemName=val;if(!val){R.warn='itemName 추출 실패';R.ok=true;return JSON.stringify(R);}if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={};window.__WORKFLOW_CTX__.itemName=val;R.ok=true;R.info='캐처: '+val;return JSON.stringify(R);})()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CAPTURE_PRICE_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.ok=true;R.info='warn: 단가관리 테이블에 데이터 0건';if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.pages.push({name:'단가관리',rows:0});}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=[1,2,3];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.itemName=val;if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={pages:[],totalRows:0,crossPageMatches:0,perfMetrics:[],errors:[]};if(val)window.__WORKFLOW_CTX__.itemName=val;window.__WORKFLOW_CTX__.pages.push({name:'단가관리',rows:rows.length});window.__WORKFLOW_CTX__.totalRows+=rows.length;R.ok=true;R.info='pass: 단가 품목 \"'+(val||'N/A')+'\" 캡처, '+rows.length+'건 존재';return JSON.stringify(R);})()",
"phase": "CAPTURE_PRICE_ITEM" "phase": "CAPTURE_PRICE_ITEM"
}, },
{ {
"id": 8, "id": 10,
"name": "[판매 > 단가관리] 페이지 로드 성능 + JS 에러 확인",
"action": "evaluate",
"script": "(()=>{const R={phase:'PERF_단가관리'};const nav=performance.getEntriesByType('navigation')[0];R.loadTime=nav?Math.round(nav.loadEventEnd-nav.startTime):'N/A';const errors=window.__CONSOLE_ERRORS__||[];R.jsErrors=errors.length;if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.perfMetrics.push({page:'단가관리',loadTime:R.loadTime});window.__WORKFLOW_CTX__.errors.push({page:'단가관리',count:errors.length});}R.ok=true;R.info='pass: page load '+R.loadTime+'ms, '+errors.length+' JS errors';return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
},
{
"id": 11,
"name": "[판매 > 수주관리] 메뉴 이동", "name": "[판매 > 수주관리] 메뉴 이동",
"action": "menu_navigate", "action": "menu_navigate",
"level1": "판매관리", "level1": "판매관리",
@@ -70,26 +94,34 @@
"timeout": 10000 "timeout": 10000
}, },
{ {
"id": 9, "id": 12,
"name": "[판매 > 수주관리] wait", "name": "[판매 > 수주관리] wait",
"action": "wait", "action": "wait",
"timeout": 3000 "timeout": 3000
}, },
{ {
"id": 10, "id": 13,
"name": "[판매 > 수주관리] wait_for_table", "name": "[판매 > 수주관리] wait_for_table",
"action": "wait_for_table", "action": "wait_for_table",
"timeout": 5000 "timeout": 5000
}, },
{ {
"id": 11, "id": 14,
"name": "[판매 > 수주관리] CHECK_ORDERS", "name": "[판매 > 수주관리] CHECK_ORDERS + 거래처 교차 검증",
"action": "evaluate", "action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHECK_ORDERS'};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);})()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHECK_ORDERS'};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.ok=true;R.info='warn: 수주관리 테이블에 데이터 0건';if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.pages.push({name:'수주관리',rows:0});}return JSON.stringify(R);}const vendorName=window.__E2E_VENDOR__||window.__WORKFLOW_CTX__?.clientName||'';R.vendorSearched=vendorName;let vendorFound=false;if(vendorName){vendorFound=Array.from(rows).some(r=>r.innerText?.includes(vendorName));}R.vendorFoundInOrders=vendorFound;if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={pages:[],totalRows:0,crossPageMatches:0,perfMetrics:[],errors:[]};window.__WORKFLOW_CTX__.pages.push({name:'수주관리',rows:rows.length,vendorFound});window.__WORKFLOW_CTX__.totalRows+=rows.length;if(vendorFound)window.__WORKFLOW_CTX__.crossPageMatches++;R.ok=true;R.info='pass: 수주 '+rows.length+'건 존재'+(vendorName?(vendorFound?' + 거래처 \"'+vendorName+'\" 발견':' + 거래처 \"'+vendorName+'\" 미발견'):'');return JSON.stringify(R);})()",
"phase": "CHECK_ORDERS" "phase": "CHECK_ORDERS"
}, },
{ {
"id": 12, "id": 15,
"name": "[판매 > 수주관리] 페이지 로드 성능 + JS 에러 확인",
"action": "evaluate",
"script": "(()=>{const R={phase:'PERF_수주관리'};const nav=performance.getEntriesByType('navigation')[0];R.loadTime=nav?Math.round(nav.loadEventEnd-nav.startTime):'N/A';const errors=window.__CONSOLE_ERRORS__||[];R.jsErrors=errors.length;if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.perfMetrics.push({page:'수주관리',loadTime:R.loadTime});window.__WORKFLOW_CTX__.errors.push({page:'수주관리',count:errors.length});}R.ok=true;R.info='pass: page load '+R.loadTime+'ms, '+errors.length+' JS errors';return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
},
{
"id": 16,
"name": "[회계 > 매출관리] 메뉴 이동", "name": "[회계 > 매출관리] 메뉴 이동",
"action": "menu_navigate", "action": "menu_navigate",
"level1": "회계관리", "level1": "회계관리",
@@ -97,23 +129,49 @@
"timeout": 10000 "timeout": 10000
}, },
{ {
"id": 13, "id": 17,
"name": "[회계 > 매출관리] wait", "name": "[회계 > 매출관리] wait",
"action": "wait", "action": "wait",
"timeout": 3000 "timeout": 3000
}, },
{ {
"id": 14, "id": 18,
"name": "[회계 > 매출관리] wait_for_table", "name": "[회계 > 매출관리] wait_for_table",
"action": "wait_for_table", "action": "wait_for_table",
"timeout": 5000 "timeout": 5000
}, },
{ {
"id": 15, "id": 19,
"name": "[회계 > 매출관리] CHECK_SALES", "name": "[회계 > 매출관리] CHECK_SALES + 거래처 교차 검증",
"action": "evaluate", "action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHECK_SALES'};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);})()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHECK_SALES'};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.ok=true;R.info='warn: 매출관리 테이블에 데이터 0건';if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.pages.push({name:'매출관리',rows:0});}return JSON.stringify(R);}const vendorName=window.__E2E_VENDOR__||window.__WORKFLOW_CTX__?.clientName||'';R.vendorSearched=vendorName;let vendorFound=false;if(vendorName){vendorFound=Array.from(rows).some(r=>r.innerText?.includes(vendorName));}R.vendorFoundInSales=vendorFound;if(!window.__WORKFLOW_CTX__)window.__WORKFLOW_CTX__={pages:[],totalRows:0,crossPageMatches:0,perfMetrics:[],errors:[]};window.__WORKFLOW_CTX__.pages.push({name:'매출관리',rows:rows.length,vendorFound});window.__WORKFLOW_CTX__.totalRows+=rows.length;if(vendorFound)window.__WORKFLOW_CTX__.crossPageMatches++;R.ok=true;R.info='pass: 매출 '+rows.length+'건 존재'+(vendorName?(vendorFound?' + 거래처 \"'+vendorName+'\" 발견':' + 거래처 \"'+vendorName+'\" 미발견'):'');return JSON.stringify(R);})()",
"phase": "CHECK_SALES" "phase": "CHECK_SALES"
},
{
"id": 20,
"name": "[회계 > 매출관리] 페이지 로드 성능 + JS 에러 확인",
"action": "evaluate",
"script": "(()=>{const R={phase:'PERF_매출관리'};const nav=performance.getEntriesByType('navigation')[0];R.loadTime=nav?Math.round(nav.loadEventEnd-nav.startTime):'N/A';const errors=window.__CONSOLE_ERRORS__||[];R.jsErrors=errors.length;if(window.__WORKFLOW_CTX__){window.__WORKFLOW_CTX__.perfMetrics.push({page:'매출관리',loadTime:R.loadTime});window.__WORKFLOW_CTX__.errors.push({page:'매출관리',count:errors.length});}R.ok=true;R.info='pass: page load '+R.loadTime+'ms, '+errors.length+' JS errors';return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
},
{
"id": 21,
"name": "[VERIFY] 콘솔 에러 전체 확인",
"description": "전체 워크플로우 동안 발생한 JS 콘솔 에러 확인",
"action": "evaluate",
"script": "(()=>{const R={phase:'CONSOLE_CHECK'};const errors=window.__CONSOLE_ERRORS__||[];R.totalErrors=errors.length;if(errors.length===0){R.info='pass: 0 JS console errors across all pages';R.ok=true;}else{R.info='warn: '+errors.length+' JS errors total: '+errors.slice(0,5).join(' | ').substring(0,300);R.errors=errors.slice(0,10);R.ok=true;}return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "VERIFY"
},
{
"id": 22,
"name": "[SUMMARY] 워크플로우 최종 요약",
"description": "4개 페이지 방문 결과, 전체 행 수, 교차 검증 결과, 성능 메트릭, JS 에러를 종합 보고",
"action": "evaluate",
"script": "(()=>{const ctx=window.__WORKFLOW_CTX__||{};const R={phase:'WORKFLOW_SUMMARY'};R.pagesVisited=(ctx.pages||[]).length;R.totalRows=ctx.totalRows||0;R.crossPageMatches=ctx.crossPageMatches||0;R.vendorName=ctx.clientName||'N/A';R.itemName=ctx.itemName||'N/A';R.pageDetails=(ctx.pages||[]).map(p=>p.name+': '+p.rows+'rows'+(p.vendorFound?' (vendor found)':'')).join(' | ');R.perfSummary=(ctx.perfMetrics||[]).map(p=>p.page+': '+p.loadTime+'ms').join(' | ');const totalErrors=(ctx.errors||[]).reduce((s,e)=>s+e.count,0);R.totalJsErrors=totalErrors;R.errorDetails=(ctx.errors||[]).map(e=>e.page+': '+e.count).join(' | ');const allPagesHaveData=(ctx.pages||[]).every(p=>p.rows>0);R.allPagesHaveData=allPagesHaveData;R.ok=true;R.info='Workflow complete: '+R.pagesVisited+' pages, '+R.totalRows+' total rows, '+R.crossPageMatches+' cross-page vendor matches, '+(allPagesHaveData?'all pages have data':'some pages empty')+', '+totalErrors+' JS errors';return JSON.stringify(R);})()",
"timeout": 5000,
"phase": "SUMMARY"
} }
] ]
} }