{ "id": "approval-box", "name": "결재함 E2E 테스트", "screenshotPolicy": { "onErrorOnly": true, "captureOn": [ "error", "fail", "timeout", "404", "500", "blocked" ] }, "description": "결재함 페이지의 전체 기능을 검증합니다 (탭 전환, 검색, 필터, 승인/반려, 모달)", "baseUrl": "https://dev.codebridge-x.com", "menuNavigation": { "level1": "결재관리", "level2": "결재함", "expectedUrl": "/ko/approval/inbox", "searchWithinParent": true, "closeOtherMenus": true }, "auth": { "username": "TestUser5", "password": "password123!" }, "navigation": { "targetUrl": "/approval/inbox", "urlPattern": "/approval/inbox|/ko/approval/inbox", "menuHints": [ "결재함", "결재 함", "결재관리" ] }, "menuNavigationEnhanced": { "strategy": "scroll-and-search", "description": "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지", "level1": "결재관리", "level2": "결재함", "alternativeLevel2Names": [ "결재함", "결재 함", "승인함", "Approval Box", "inbox" ], "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": [ { "id": 1, "name": "사이드바 메뉴 전체 펼치기", "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비", "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'; })()" }, { "id": 2, "name": "결재관리 > 결재함 메뉴 진입", "description": "사이드바를 스크롤하며 '결재관리' > '결재함' 메뉴를 찾아 클릭", "action": "menu_navigate", "level1": "결재관리", "level2": "결재함" }, { "id": 3, "name": "메뉴 도착 확인", "description": "결재함 페이지에 도착했는지 확인", "action": "verify_url", "target": "/approval/inbox" }, { "id": 4, "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'; })()" }, { "id": 5, "name": "페이지 정상 로드 확인", "description": "결재함 페이지가 정상적으로 로드되었는지 확인", "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'; })()" }, { "id": 6, "name": "통계 카드 확인", "action": "verify_element", "target": "[class*='card'], [class*='stat']" }, { "id": 7, "name": "탭 구조 확인", "action": "verify_element", "target": "[role='tab'], button[role='tab']" }, { "id": 8, "name": "테이블 데이터 확인", "action": "verify_table", "target": "table" }, { "id": 9, "name": "목록 필터 테스트", "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 })()" }, { "id": 10, "name": "필수 검증: 결재 문서 상세 보기", "description": "테이블에서 결재 문서 클릭하여 상세 모달/페이지 확인", "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'; })()", "note": "결재 문서가 없으면 데이터 생성 또는 SKIP" }, { "id": 11, "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'; })()" }, { "id": 12, "name": "필수 검증: PDF 다운로드 실행", "description": "PDF 다운로드 버튼 클릭 및 API 응답 확인", "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'; })()" }, { "id": 13, "name": "PDF 파일 유효성 검증", "description": "다운로드된 PDF 파일의 기본 유효성 검사", "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'; })()" }, { "id": 14, "name": "PDF 스타일 수동 확인 체크리스트", "description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목", "action": "evaluate", "script": "(() => { return 'Manual check items: 테이블 경계선, 한글 폰트, 숫자 정렬, 여백, 헤더/푸터, 로고/이미지, 페이지 나눔, 배경색, 텍스트 오버플로우, 결재선 표시'; })()", "manualChecklist": [ { "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": "결재선" } ] }, { "id": 15, "name": "필수 검증: 결재 승인 실제 수행", "description": "미결재 문서에 대해 실제 승인 처리 수행", "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": "버튼 존재만 확인하면 불완전! 실제 승인까지 검증 필수!" }, { "id": 16, "name": "결재 승인 결과 확인", "description": "승인 후 결재완료 탭에서 해당 문서 확인", "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'; })()", "verify": { "documentMoved": "승인한 문서가 결재완료 탭에 표시", "statusUpdated": "결재 상태가 '완료'로 변경" } }, { "id": 17, "name": "필수 검증: 결재 반려 실제 수행", "description": "미결재 문서에 대해 실제 반려 처리 수행", "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'; })()", "note": "반려 버튼 존재만 확인하면 불완전! 실제 반려까지 검증 필수!" }, { "id": 18, "name": "결재 반려 결과 확인", "description": "반려 후 결재반려 탭에서 해당 문서 확인", "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'; })()", "verify": { "documentMoved": "반려한 문서가 결재반려 탭에 표시", "statusUpdated": "결재 상태가 '반려'로 변경", "rejectReason": "반려 사유가 표시" } }, { "id": 19, "name": "검색 기능 테스트", "description": "검색 필터로 결재 문서 검색", "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'; })()" }, { "id": 20, "name": "콘솔 에러 확인", "action": "verify_element", "target": "body" } ], "mandatoryVerifications": { "description": "E2E_TEST_CONFIG.md 기준 필수 검증 항목", "items": [ { "id": 4, "name": "결재 승인/반려 완료", "trigger": "결재 문서 상세의 승인/반려 버튼", "verification": "실제 승인/반려 동작 + API 호출 + 결과 확인", "failCondition": "버튼 존재만 확인, 클릭하지 않음", "steps": [ "9", "10" ] } ] }, "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": [ "404 방지: 반드시 메뉴 클릭으로 페이지 진입 (직접 URL 접근 금지)", "스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음", "대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색", "메뉴 계층: 결재관리 > 결재함", "탭 전환 시 URL 변경 없이 데이터만 필터링됨" ] }