2026-01-30 10:50:38 +09:00
{
2026-02-04 21:59:56 +09:00
"id" : "approval-box" ,
2026-01-30 10:50:38 +09:00
"name" : "결재함 E2E 테스트" ,
2026-03-01 19:13:24 +09:00
"version" : "2.0.0" ,
2026-01-30 10:50:38 +09:00
"screenshotPolicy" : {
"onErrorOnly" : true ,
2026-02-06 01:26:59 +09:00
"captureOn" : [
"error" ,
"fail" ,
"timeout" ,
"404" ,
"500" ,
"blocked"
]
2026-01-30 10:50:38 +09:00
} ,
2026-03-01 19:13:24 +09:00
"description" : "결재함 페이지의 전체 기능을 검증합니다 (탭 전환, 검색, 필터, 승인/반려, 모달, 탭 카운트 변화, PDF 다운로드, 상태 전이)" ,
2026-01-30 10:50:38 +09:00
"baseUrl" : "https://dev.codebridge-x.com" ,
2026-02-04 21:59:56 +09:00
"menuNavigation" : {
"level1" : "결재관리" ,
"level2" : "결재함" ,
"expectedUrl" : "/ko/approval/inbox" ,
"searchWithinParent" : true ,
"closeOtherMenus" : true
} ,
"auth" : {
"username" : "TestUser5" ,
"password" : "password123!"
} ,
2026-01-30 21:47:29 +09:00
"navigation" : {
"targetUrl" : "/approval/inbox" ,
"urlPattern" : "/approval/inbox|/ko/approval/inbox" ,
2026-02-06 01:26:59 +09:00
"menuHints" : [
"결재함" ,
"결재 함" ,
"결재관리"
]
2026-01-30 21:47:29 +09:00
} ,
2026-01-30 10:50:38 +09:00
"menuNavigationEnhanced" : {
"strategy" : "scroll-and-search" ,
"description" : "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지" ,
"level1" : "결재관리" ,
"level2" : "결재함" ,
2026-02-06 01:26:59 +09:00
"alternativeLevel2Names" : [
"결재함" ,
"결재 함" ,
"승인함" ,
"Approval Box" ,
"inbox"
] ,
2026-01-30 10:50:38 +09:00
"fallbackUrls" : [
"/ko/approval/inbox" ,
"/ko/approval/box" ,
"/ko/approvals/inbox" ,
"/ko/approval-management/inbox" ,
"/approval/inbox"
] ,
"scrollConfig" : {
"sidebarSelector" : "nav, aside, [role='navigation'], .sidebar, #sidebar" ,
"menuItemSelector" : "a, button, [role='menuitem'], [role='treeitem']" ,
"scrollStep" : 200 ,
"maxScrollAttempts" : 10 ,
"scrollDelay" : 300
}
} ,
"steps" : [
{
2026-02-09 15:05:03 +09:00
"id" : 1 ,
2026-01-30 16:26:52 +09:00
"name" : "사이드바 메뉴 전체 펼치기" ,
"description" : "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
"script" : "(async () => { const sidebar = document.querySelector('.sidebar-scroll, [class*=\"sidebar\"], nav'); if (sidebar) sidebar.scrollTo({top: 0, behavior: 'instant'}); await new Promise(r => setTimeout(r, 300)); Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click(); await new Promise(r => setTimeout(r, 2000)); return 'Menu expanded'; })()"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 2 ,
2026-02-28 17:21:01 +09:00
"name" : "결재관리 > 결재함 메뉴 진입" ,
"description" : "사이드바를 스크롤하며 '결재관리' > '결재함' 메뉴를 찾아 클릭" ,
"action" : "menu_navigate" ,
"level1" : "결재관리" ,
"level2" : "결재함"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 3 ,
2026-02-28 17:21:01 +09:00
"name" : "메뉴 도착 확인" ,
"description" : "결재함 페이지에 도착했는지 확인" ,
"action" : "verify_url" ,
"target" : "/approval/inbox"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 4 ,
2026-02-28 17:21:01 +09:00
"name" : "404 에러 감지" ,
"description" : "페이지 로드 후 404 에러 여부 확인" ,
"action" : "evaluate" ,
"script" : "(async () => { await new Promise(r => setTimeout(r, 1000)); const indicators = ['페이지를 찾을 수 없습니다', '404', 'Not Found', '존재하지 않거나']; const bodyText = document.body.innerText || ''; const found = indicators.find(i => bodyText.includes(i)); if (found) return 'WARN: 404 detected - ' + found; return 'PASS: No 404 error'; })()"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 5 ,
2026-01-30 10:50:38 +09:00
"name" : "페이지 정상 로드 확인" ,
"description" : "결재함 페이지가 정상적으로 로드되었는지 확인" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
"script" : "(() => { const bodyText = document.body.innerText || ''; const titleCheck = ['결재함', '결재', 'Approval'].some(t => bodyText.includes(t)); const no404 = !['404', '찾을 수 없습니다', 'Not Found'].some(t => bodyText.includes(t)); if (titleCheck && no404) return 'PASS: Page loaded correctly'; if (!titleCheck) return 'WARN: Page title not found'; return 'FAIL: 404 error detected'; })()"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 6 ,
2026-01-30 10:50:38 +09:00
"name" : "통계 카드 확인" ,
2026-02-06 00:46:05 +09:00
"action" : "verify_element" ,
2026-02-28 17:21:01 +09:00
"target" : "[class*='card'], [class*='stat']"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 7 ,
2026-01-30 10:50:38 +09:00
"name" : "탭 구조 확인" ,
2026-02-06 00:46:05 +09:00
"action" : "verify_element" ,
2026-02-28 17:21:01 +09:00
"target" : "[role='tab'], button[role='tab']"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 8 ,
2026-01-30 10:50:38 +09:00
"name" : "테이블 데이터 확인" ,
2026-02-06 01:26:59 +09:00
"action" : "verify_table" ,
2026-02-28 17:21:01 +09:00
"target" : "table"
2026-01-30 10:50:38 +09:00
} ,
{
2026-02-09 15:05:03 +09:00
"id" : 9 ,
"name" : "목록 필터 테스트" ,
"action" : "evaluate" ,
2026-03-01 19:13:24 +09:00
"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)'; })()"
2026-02-09 15:05:03 +09:00
} ,
{
"id" : 10 ,
2026-03-01 19:13:24 +09:00
"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 ,
2026-02-28 17:21:01 +09:00
"name" : "필수 검증: 결재 문서 상세 보기" ,
2026-01-30 10:50:38 +09:00
"description" : "테이블에서 결재 문서 클릭하여 상세 모달/페이지 확인" ,
2026-02-28 17:21:01 +09:00
"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)); } const row = document.querySelector('table tbody tr'); if (row) { row.click(); await new Promise(r => setTimeout(r, 1000)); } const bodyText = document.body.innerText || ''; const hasDetail = ['문서 제목', '기안자', '기안일', '결재 상태', '승인', '반려'].some(t => bodyText.includes(t)); return hasDetail ? 'PASS: Detail view opened' : 'WARN: Detail view not confirmed'; })()" ,
2026-01-30 10:50:38 +09:00
"note" : "결재 문서가 없으면 데이터 생성 또는 SKIP"
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 12 ,
2026-02-28 17:21:01 +09:00
"name" : "PDF 다운로드 전 모달 상태 확인" ,
"description" : "PDF 생성 전 모달 상태를 확인하여 CSS 문제 감지용 기준 확보" ,
"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'; })()"
2026-01-30 10:50:38 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 13 ,
2026-02-28 17:21:01 +09:00
"name" : "필수 검증: PDF 다운로드 실행" ,
"description" : "PDF 다운로드 버튼 클릭 및 API 응답 확인" ,
"action" : "evaluate" ,
2026-03-04 11:42:23 +09:00
"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.__E2E__?window.__E2E__.getApiLogs().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'; })()"
2026-01-30 10:50:38 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 14 ,
"name" : "PDF 파일 유효성 + content-type 검증" ,
"description" : "다운로드된 PDF API 응답의 status 200 및 content-type 확인" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
2026-03-04 11:42:23 +09:00
"script" : "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().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);})()" ,
2026-03-01 19:13:24 +09:00
"timeout" : 5000 ,
"phase" : "VERIFY"
2026-01-30 10:50:38 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 15 ,
2026-02-28 17:21:01 +09:00
"name" : "PDF 스타일 수동 확인 체크리스트" ,
2026-01-30 10:50:38 +09:00
"description" : "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
"script" : "(() => { return 'Manual check items: 테이블 경계선, 한글 폰트, 숫자 정렬, 여백, 헤더/푸터, 로고/이미지, 페이지 나눔, 배경색, 텍스트 오버플로우, 결재선 표시'; })()" ,
2026-01-30 10:50:38 +09:00
"manualChecklist" : [
2026-02-06 01:26:59 +09:00
{
"id" : "css-1" ,
"item" : "테이블 경계선이 올바르게 표시되는가?" ,
"category" : "테이블 스타일"
} ,
{
"id" : "css-2" ,
"item" : "한글 폰트가 깨지지 않고 정상 표시되는가?" ,
"category" : "폰트"
} ,
{
"id" : "css-3" ,
"item" : "숫자/금액 정렬이 올바른가? (우측 정렬)" ,
"category" : "정렬"
} ,
{
"id" : "css-4" ,
"item" : "여백(margin/padding)이 적절한가?" ,
"category" : "레이아웃"
} ,
{
"id" : "css-5" ,
"item" : "헤더/푸터가 각 페이지에 올바르게 표시되는가?" ,
"category" : "페이지 구조"
} ,
{
"id" : "css-6" ,
"item" : "로고/이미지가 정상 표시되는가?" ,
"category" : "이미지"
} ,
{
"id" : "css-7" ,
"item" : "페이지 나눔(page break)이 적절한 위치에서 발생하는가?" ,
"category" : "페이지 나눔"
} ,
{
"id" : "css-8" ,
"item" : "배경색/강조색이 올바르게 적용되었는가?" ,
"category" : "색상"
} ,
{
"id" : "css-9" ,
"item" : "텍스트가 잘리거나 겹치지 않는가?" ,
"category" : "오버플로우"
} ,
{
"id" : "css-10" ,
"item" : "결재선 정보가 정상적으로 표시되는가?" ,
"category" : "결재선"
}
2026-02-28 17:21:01 +09:00
]
2026-01-30 10:50:38 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 16 ,
2026-02-28 17:21:01 +09:00
"name" : "필수 검증: 결재 승인 실제 수행" ,
2026-01-30 10:50:38 +09:00
"description" : "미결재 문서에 대해 실제 승인 처리 수행" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
"script" : "(async () => { const approveBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('승인')); if (!approveBtn) return 'WARN: Approve button not found'; approveBtn.click(); await new Promise(r => setTimeout(r, 500)); const confirmBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('확인')); if (confirmBtn) { confirmBtn.click(); await new Promise(r => setTimeout(r, 1000)); } return 'Approval action attempted'; })()" ,
"note" : "버튼 존재만 확인하면 불완전! 실제 승인까지 검증 필수!"
2026-01-30 10:50:38 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 17 ,
"name" : "[APPROVAL] 승인 후 탭 카운트 변화 검증" ,
"description" : "승인 수행 후 대기 탭 감소 + 완료 탭 증가를 검증" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
2026-03-01 19:13:24 +09:00
"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" ,
2026-01-30 10:50:38 +09:00
"verify" : {
"documentMoved" : "승인한 문서가 결재완료 탭에 표시" ,
"statusUpdated" : "결재 상태가 '완료'로 변경"
}
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 19 ,
"name" : "[APPROVAL] 승인 후 결재 버튼 비활성 확인" ,
"description" : "이미 승인된 문서를 다시 열어 승인 버튼이 비활성/미표시인지 확인" ,
"action" : "evaluate" ,
"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로 정확히 입력" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
2026-03-01 19:13:24 +09:00
"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" ,
2026-02-28 17:21:01 +09:00
"note" : "반려 버튼 존재만 확인하면 불완전! 실제 반려까지 검증 필수!"
2026-01-30 10:50:38 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 22 ,
"name" : "[REJECT] 결재반려 탭 이동 + 반려 문서 확인" ,
"description" : "결재반려 탭으로 이동하여 반려한 문서와 반려 사유가 존재하는지 확인" ,
2026-02-28 17:21:01 +09:00
"action" : "evaluate" ,
2026-03-01 19:13:24 +09:00
"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" ,
2026-01-30 10:50:38 +09:00
"verify" : {
"documentMoved" : "반려한 문서가 결재반려 탭에 표시" ,
"statusUpdated" : "결재 상태가 '반려'로 변경" ,
"rejectReason" : "반려 사유가 표시"
}
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 23 ,
2026-01-30 10:50:38 +09:00
"name" : "검색 기능 테스트" ,
"description" : "검색 필터로 결재 문서 검색" ,
2026-02-28 17:21:01 +09:00
"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'; })()"
2026-02-09 15:05:03 +09:00
} ,
{
2026-03-01 19:13:24 +09:00
"id" : 24 ,
"name" : "[VERIFY] 콘솔 에러 확인" ,
"description" : "JS 콘솔에 심각한 에러가 없는지 확인" ,
"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" ,
2026-03-04 11:42:23 +09:00
"script" : "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().logs:[]);const errors=(window.__E2E__?window.__E2E__.getApiLogs().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);})()" ,
2026-03-01 19:13:24 +09:00
"timeout" : 5000 ,
"phase" : "VERIFY"
2026-01-30 10:50:38 +09:00
}
] ,
"mandatoryVerifications" : {
"description" : "E2E_TEST_CONFIG.md 기준 필수 검증 항목" ,
"items" : [
{
"id" : 4 ,
"name" : "결재 승인/반려 완료" ,
"trigger" : "결재 문서 상세의 승인/반려 버튼" ,
"verification" : "실제 승인/반려 동작 + API 호출 + 결과 확인" ,
"failCondition" : "버튼 존재만 확인, 클릭하지 않음" ,
2026-02-06 01:26:59 +09:00
"steps" : [
2026-03-01 19:13:24 +09:00
"16" ,
"17" ,
"21" ,
"22"
2026-02-06 01:26:59 +09:00
]
2026-01-30 10:50:38 +09:00
}
]
} ,
"expectedAPIs" : [
"GET /api/v1/approvals/inbox - 결재함 목록 조회" ,
"GET /api/v1/approvals/inbox/summary - 결재함 통계" ,
"GET /api/v1/approvals/{id} - 결재 문서 상세 조회" ,
"POST /api/v1/approvals/{id}/approve - 결재 승인" ,
"POST /api/v1/approvals/{id}/reject - 결재 반려"
] ,
"notes" : [
2026-02-28 17:21:01 +09:00
"404 방지: 반드시 메뉴 클릭으로 페이지 진입 (직접 URL 접근 금지)" ,
"스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음" ,
"대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색" ,
2026-01-30 10:50:38 +09:00
"메뉴 계층: 결재관리 > 결재함" ,
2026-03-01 19:13:24 +09:00
"탭 전환 시 URL 변경 없이 데이터만 필터링됨" ,
"v2.0: 탭 카운트 비교, 상태 전이 검증, 반려 사유 입력, PDF content-type 검증, 승인 후 버튼 비활성 확인 추가"
2026-01-30 10:50:38 +09:00
]
2026-02-28 17:21:01 +09:00
}