{ "id": "attendance-checkin", "name": "근태현황 출퇴근 테스트", "screenshotPolicy": { "onErrorOnly": true, "captureOn": [ "error", "fail", "timeout", "404", "500", "blocked" ] }, "description": "위치 정보 권한 허용 후 출근/퇴근 기록을 테스트하는 E2E 테스트", "baseUrl": "https://dev.codebridge-x.com", "url": "/hr/attendance", "navigation": { "targetUrl": "/hr/attendance", "urlPattern": "/hr/attendance|/ko/hr/attendance", "menuHints": [ "근태현황", "근태 현황", "출퇴근", "인사관리" ] }, "menuNavigation": { "level1": "인사관리", "level2": "근태현황", "expectedUrl": "/hr/attendance", "searchWithinParent": true, "closeOtherMenus": true }, "menuNavigationEnhanced": { "strategy": "scroll-and-search", "description": "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지", "level1": "인사관리", "level2": "근태현황", "alternativeLevel1Names": [ "인사관리", "인사 관리", "HR", "Human Resource", "HR관리" ], "alternativeLevel2Names": [ "근태현황", "근태 현황", "출퇴근", "Attendance", "출퇴근현황", "근태관리" ], "fallbackUrls": [ "/hr/attendance", "/ko/hr/attendance", "/ko/hr/attendance-status", "/ko/hr/checkin", "/ko/human-resource/attendance" ], "scrollConfig": { "sidebarSelector": "nav, aside, [role='navigation'], .sidebar, #sidebar", "menuItemSelector": "a, button, [role='menuitem'], [role='treeitem']", "scrollStep": 200, "maxScrollAttempts": 10, "scrollDelay": 300 } }, "timeout": 120000, "tags": [ "hr", "attendance", "geolocation", "checkin", "checkout" ], "auth": { "username": "TestUser5", "password": "password123!" }, "browserConfig": { "permissions": { "geolocation": { "grant": true, "description": "위치 정보 접근 권한 허용 - 출퇴근 기록에 필수", "mockLocation": { "enabled": true, "latitude": 37.557358, "longitude": 126.864414, "accuracy": 100 } } }, "contextOptions": { "geolocation": { "latitude": 37.557358, "longitude": 126.864414 }, "permissions": [ "geolocation" ] } }, "preTestSetup": { "description": "테스트 시작 전 Playwright 브라우저 컨텍스트에서 위치 권한 설정", "playwright": { "grantPermissions": [ "geolocation" ], "setGeolocation": { "latitude": 37.557358, "longitude": 126.864414, "accuracy": 100 }, "code": [ "// Playwright MCP 사용 시 브라우저 시작 직후 실행", "// mcp__playwright__playwright_evaluate로 위치 권한 자동 허용", "await context.grantPermissions(['geolocation']);", "await context.setGeolocation({ latitude: 37.557358, longitude: 126.864414 });" ] } }, "steps": [ { "id": 1, "name": "Geolocation API 모킹 (권한 팝업 방지)", "description": "페이지 로드 직후 Geolocation API를 모킹하여 브라우저 권한 팝업이 나타나지 않도록 함", "action": "evaluate", "script": "(async () => { const mockPosition = { coords: { latitude: 37.557358, longitude: 126.864414, accuracy: 100, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }; const mockGeolocation = { getCurrentPosition: (success, error, options) => { console.log('[E2E] Geolocation.getCurrentPosition - 모킹된 위치 반환'); setTimeout(() => success(mockPosition), 50); }, watchPosition: (success, error, options) => { console.log('[E2E] Geolocation.watchPosition - 모킹된 위치 반환'); setTimeout(() => success(mockPosition), 50); return 1; }, clearWatch: (id) => {} }; Object.defineProperty(navigator, 'geolocation', { value: mockGeolocation, writable: false, configurable: true }); console.log('[E2E] Geolocation API 모킹 완료 - 서울 영등포구 좌표'); await new Promise(r => setTimeout(r, 300)); return JSON.stringify({ success: true, coords: mockPosition.coords }); })()", "note": "Geolocation API를 모킹하면 브라우저가 위치 권한을 요청하지 않음" }, { "id": 2, "name": "브라우저 위치 권한 팝업 클릭 (좌측 상단)", "description": "Chrome 브라우저 좌측 상단에 나타나는 '사이트에 있는 동안 허용' 팝업 클릭", "action": "evaluate", "script": "(async function() { await new Promise(r => setTimeout(r, 1500)); const permissionSelectors = [ '[class*=\"permission\"][class*=\"allow\"]', '[class*=\"infobar\"] button', '[aria-label*=\"허용\"]', '[aria-label*=\"Allow\"]', 'button:has-text(\"사이트에 있는 동안 허용\")', 'button:has-text(\"허용\")', 'button:has-text(\"Allow\")', '[data-testid*=\"permission\"]', '.permission-prompt button', '[class*=\"PermissionPrompt\"] button' ]; for (const sel of permissionSelectors) { try { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { btn.click(); console.log('[E2E] 위치 권한 팝업 클릭 성공:', sel); await new Promise(r => setTimeout(r, 500)); return JSON.stringify({ clicked: true, selector: sel }); } } catch(e) {} } const allButtons = Array.from(document.querySelectorAll('button, [role=\"button\"]')); const allowBtn = allButtons.find(b => { const text = b.innerText || b.textContent || ''; return text.includes('사이트에 있는 동안 허용') || text.includes('허용') || text.includes('Allow'); }); if (allowBtn && allowBtn.offsetParent !== null) { allowBtn.click(); console.log('[E2E] 위치 권한 팝업 텍스트 검색으로 클릭'); return JSON.stringify({ clicked: true, method: 'textSearch' }); } console.log('[E2E] 위치 권한 팝업 없음 (이미 허용되었거나 모킹으로 우회됨)'); await new Promise(r => setTimeout(r, 500)); return JSON.stringify({ clicked: false, reason: 'no_popup_found' }); })()", "errorHandling": { "onTimeout": "continue", "onNotFound": "continue", "reason": "팝업이 없으면 이미 허용된 상태로 간주" } }, { "id": 3, "name": "사이드바 메뉴 전체 펼치기", "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴 펼침", "action": "evaluate", "script": "(async () => { document.querySelector('.sidebar-scroll')?.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": 4, "name": "1차 메뉴 찾기: 인사관리 (스크롤 포함)", "description": "사이드바를 스크롤하며 '인사관리' 메뉴를 찾아 클릭", "action": "menu_navigate", "level1": "인사관리", "level2": "근태현황" }, { "id": 5, "name": "2차 메뉴 도착 확인", "description": "근태현황 페이지에 도착했는지 확인", "action": "verify_url", "target": "/hr/attendance" }, { "id": 6, "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": 7, "name": "페이지 정상 로드 확인", "description": "근태현황 페이지가 정상적으로 로드되었는지 확인", "action": "evaluate", "script": "(() => { const bodyText = document.body.innerText || ''; const titleCheck = ['근태현황', '출퇴근', 'Attendance'].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": 8, "name": "브라우저 위치 권한 설정", "description": "Playwright context에서 위치 정보 권한을 허용하고 가상 위치 설정", "action": "evaluate", "script": "(() => { console.log('[E2E] Geolocation permission should be granted via Playwright context.grantPermissions'); return 'Geolocation permission note: handled by Playwright context'; })()" }, { "id": 9, "name": "위치 정보 로딩 대기", "description": "Google Map 로딩 및 현재 위치 표시 대기", "action": "wait_for_element", "target": "region[name='지도'], [class*='map'], canvas, iframe[src*='map']", "timeout": 10000 }, { "id": 10, "name": "사용자 정보 확인", "description": "출퇴근 패널에서 로그인한 사용자 정보 확인", "action": "verify_element", "target": "body", "verify": { "userInfo": { "name": "홍킬동", "department": "부서명 · 개발중인 메뉴" } } }, { "id": 11, "name": "출근 상태 확인", "description": "현재 출퇴근 상태 확인 (출근 전/출근 후)", "action": "evaluate", "script": "(() => { const bodyText = document.body.innerText || ''; if (document.querySelector(\"button\") && Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('출근하기'))) return 'not_checked_in'; if (bodyText.includes('출근 완료')) return 'checked_in'; if (Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('퇴근하기'))) return 'ready_to_checkout'; return 'unknown'; })()" }, { "id": 12, "name": "출근하기 (미출근 상태인 경우)", "description": "출근하기 버튼이 활성화된 경우 클릭하여 출근 기록", "action": "click_if_exists", "target": "출근하기", "condition": { "if": "{attendanceStatus} == 'not_checked_in'" } }, { "id": 13, "name": "출근 완료 상태 확인", "description": "출근 완료 후 상태 및 출근 시간 표시 확인", "action": "verify_element", "target": "body", "verify": { "visible": [ "출근 완료" ], "buttonState": { "출근하기": "hidden_or_disabled", "퇴근하기": "enabled_or_visible" } } }, { "id": 14, "name": "퇴근하기 버튼 상태 확인", "description": "출근 완료 후 퇴근하기 버튼 활성화 여부 확인", "action": "verify_element", "target": "body", "verify": { "button": { "target": "퇴근하기", "state": "visible", "note": "일부 시스템에서는 최소 근무 시간 후에만 활성화될 수 있음" } } }, { "id": 15, "name": "퇴근하기 (선택적)", "description": "퇴근하기 버튼이 활성화된 경우 클릭하여 퇴근 기록", "optional": true, "action": "click_if_exists", "target": "퇴근하기", "condition": { "if": "button[name='퇴근하기']:enabled" } }, { "id": 16, "name": "최종 상태 확인", "description": "출퇴근 기록 후 최종 상태 확인", "action": "verify_element", "target": "body", "verify": { "url": "/hr/attendance", "mapDisplayed": true, "attendanceRecorded": true } }, { "id": 17, "name": "콘솔 에러 확인", "action": "verify_element", "target": "body" } ], "assertions": [ { "type": "url", "expected": "/hr/attendance", "message": "근태현황 페이지에 머물러야 함" }, { "type": "permission", "name": "geolocation", "state": "granted", "message": "위치 정보 권한이 허용되어야 함" }, { "type": "elementExists", "selector": "region[name='지도']", "message": "Google Map이 표시되어야 함" }, { "type": "elementExists", "selector": "text=현재 시간", "message": "현재 시간이 표시되어야 함" } ], "cleanup": { "enabled": false, "description": "출퇴근 기록은 삭제하지 않음 (업무 데이터)", "note": "테스트 후 수동으로 관리자가 삭제 필요시 처리" }, "notes": { "testScope": "위치 권한 허용 -> 근태현황 페이지 이동 -> 출근/퇴근 기록 테스트", "antiPattern404": "직접 URL 접근 금지 - 반드시 메뉴 클릭으로 페이지 진입", "scrollRequired": "사이드바 스크롤을 통해 메뉴 항목 탐색 필수", "correctUrl": "/hr/attendance (기존 /ko/hr/attendance에서 수정됨)" }, "playwrightMcpInstructions": { "description": "Playwright MCP를 사용한 위치 권한 설정 방법", "beforeNavigation": [ "1. 브라우저 navigate 전에 위치 권한 설정이 필요함", "2. Playwright MCP는 브라우저 컨텍스트 레벨에서 권한을 설정할 수 없으므로 UI 팝업 처리 필요" ], "uiPermissionHandling": { "description": "위치 권한 팝업이 나타나면 '항상 허용' 버튼 클릭", "selectors": [ "button:has-text('항상 허용')", "button:has-text('허용')", "button:has-text('Allow')" ], "workflow": [ "1. 페이지 로드 후 1-2초 대기", "2. 위치 권한 팝업 존재 여부 확인", "3. 팝업이 있으면 '항상 허용' 버튼 클릭", "4. 팝업이 없으면 이미 권한이 허용된 상태로 간주하고 진행" ] }, "mockGeolocation": { "description": "테스트용 가상 위치 설정", "latitude": 37.557358, "longitude": 126.864414, "note": "서울 영등포구 좌표 (테스트 회사 위치 가정)" } } }