{ "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": "step-0", "name": "🔐 Geolocation API 모킹 (권한 팝업 방지)", "description": "페이지 로드 직후 Geolocation API를 모킹하여 브라우저 권한 팝업이 나타나지 않도록 함", "critical": true, "executeBeforeNavigation": false, "executeImmediately": true, "actions": [ { "type": "evaluate", "script": "(() => { 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 모킹 완료 - 서울 영등포구 좌표'); return { success: true, coords: mockPosition.coords }; })()", "description": "Geolocation API 모킹 (서울 영등포구 좌표: 37.557358, 126.864414)" }, { "type": "wait", "duration": 300, "description": "모킹 적용 대기" } ], "note": "Geolocation API를 모킹하면 브라우저가 위치 권한을 요청하지 않음" }, { "id": "step-0-1", "name": "🗺️ 브라우저 위치 권한 팝업 클릭 (좌측 상단)", "description": "Chrome 브라우저 좌측 상단에 나타나는 '사이트에 있는 동안 허용' 팝업 클릭", "critical": true, "actions": [ { "type": "wait", "duration": 1500, "description": "위치 권한 팝업 표시 대기" }, { "type": "evaluate", "script": "(async function() { 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 { 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 { clicked: true, method: 'textSearch' }; } console.log('[E2E] 위치 권한 팝업 없음 (이미 허용되었거나 모킹으로 우회됨)'); return { clicked: false, reason: 'no_popup_found' }; })()", "description": "좌측 상단 권한 팝업 찾아서 클릭" }, { "type": "wait", "duration": 500, "description": "권한 설정 적용 대기" } ], "errorHandling": { "onTimeout": "continue", "onNotFound": "continue", "reason": "팝업이 없으면 이미 허용된 상태로 간주" } }, { "id": "step-0-2", "name": "📂 사이드바 메뉴 전체 펼치기", "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴 펼침", "actions": [ { "type": "evaluate", "script": "document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'})" }, { "type": "wait", "duration": 300 }, { "type": "evaluate", "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()" }, { "type": "wait", "duration": 2000 }, { "type": "screenshot", "name": "after_permission_grant_and_menu_expanded" } ], "verification": [ "사이드바 메뉴가 펼쳐졌는지 확인" ] }, { "id": 2, "name": "1차 메뉴 찾기: 인사관리 (스크롤 포함)", "description": "사이드바를 스크롤하며 '인사관리' 메뉴를 찾아 클릭", "actions": [ { "type": "scrollAndFind", "target": "인사관리", "alternativeTexts": ["인사관리", "인사 관리", "HR", "Human Resource"], "scrollContainer": "sidebar", "maxAttempts": 10, "description": "스크롤하며 인사관리 메뉴 찾기" }, { "type": "wait", "duration": 300 }, { "type": "click", "target": "인사관리", "description": "인사관리 메뉴 클릭" }, { "type": "wait", "duration": 500, "description": "서브메뉴 펼쳐지기 대기" }, { "type": "screenshot", "name": "hr_menu_expanded" } ], "verification": [ "인사관리 메뉴가 클릭되었는지 확인", "서브메뉴가 펼쳐졌는지 확인", "하위 메뉴 항목들이 보이는지 확인" ], "fallback": { "if": "메뉴를 찾을 수 없음", "then": "사이드바 전체를 스크롤하며 재탐색" } }, { "id": 3, "name": "2차 메뉴 찾기: 근태현황 (스크롤 포함)", "description": "서브메뉴에서 '근태현황'을 찾아 클릭", "actions": [ { "type": "scrollAndFind", "target": "근태현황", "alternativeTexts": ["근태현황", "근태 현황", "출퇴근", "Attendance"], "scrollContainer": "submenu", "maxAttempts": 5, "description": "서브메뉴에서 근태현황 찾기" }, { "type": "wait", "duration": 200 }, { "type": "click", "target": "근태현황", "description": "근태현황 메뉴 클릭" }, { "type": "wait", "target": "페이지 로드 완료", "timeout": 10000 }, { "type": "screenshot", "name": "attendance_page" } ], "verification": [ "근태현황 메뉴 클릭 성공", "페이지 이동 또는 컨텐츠 로드" ] }, { "id": 4, "name": "404 에러 감지 및 대체 경로 시도", "description": "페이지 로드 후 404 에러 여부 확인, 404시 대체 경로 탐색", "actions": [ { "type": "wait", "duration": 1000 }, { "type": "checkFor404", "indicators": [ "페이지를 찾을 수 없습니다", "404", "Not Found", "존재하지 않거나" ]}, { "type": "screenshot", "name": "page_load_result" } ], "verification": [ "현재 페이지가 404인지 확인" ], "onError404": { "description": "404 에러 발생 시 대체 URL 시도", "actions": [ { "type": "log", "message": "404 감지 - 대체 경로 탐색 시작" }, { "type": "tryAlternativeUrls", "urls": [ "/ko/hr/attendance", "/ko/hr/attendance-status", "/ko/hr/checkin" ], "stopOnSuccess": true }, { "type": "ifStillFailed", "action": "navigateViaMenuClick", "description": "URL 직접 접근 실패 시 메뉴 클릭으로 재시도" } ] } }, { "id": 5, "name": "페이지 정상 로드 확인", "description": "근태현황 페이지가 정상적으로 로드되었는지 확인", "actions": [ { "type": "verify", "target": "pageTitle", "contains": ["근태현황", "출퇴근", "Attendance"] }, { "type": "verify", "target": "pageContent", "notContains": ["404", "찾을 수 없습니다", "Not Found"] } ], "verification": [ "페이지 제목 '근태현황' 또는 관련 텍스트 표시", "404 에러 메시지 미표시", "콘텐츠가 정상 렌더링됨" ], "successCriteria": { "urlPattern": "/hr/attendance", "requiredElements": ["출퇴근", "출근", "퇴근", "현재 시간"] } }, { "id": "step-5", "name": "브라우저 위치 권한 설정", "description": "Playwright context에서 위치 정보 권한을 허용하고 가상 위치 설정", "playwright": { "code": "await context.grantPermissions(['geolocation']);", "setGeolocation": { "latitude": 37.557358, "longitude": 126.864414 } }, "expect": { "permissionGranted": "geolocation" } }, { "id": "step-6", "name": "위치 정보 로딩 대기", "description": "Google Map 로딩 및 현재 위치 표시 대기", "waitFor": { "type": "element", "selector": "region[name='지도']", "timeout": 10000 }, "expect": { "mapLoaded": true, "locationMarkerVisible": true } }, { "id": "step-7", "name": "사용자 정보 확인", "description": "출퇴근 패널에서 로그인한 사용자 정보 확인", "verify": { "userInfo": { "name": "홍킬동", "department": "부서명 · 개발중인 메뉴" }, "currentTime": { "format": "HH:mm:ss", "updating": true } } }, { "id": "step-8", "name": "출근 상태 확인", "description": "현재 출퇴근 상태 확인 (출근 전/출근 후)", "capture": { "variable": "attendanceStatus", "checkElements": [ { "selector": "button:has-text('출근하기')", "status": "not_checked_in" }, { "selector": "text=출근 완료", "status": "checked_in" }, { "selector": "button:has-text('퇴근하기')", "status": "ready_to_checkout" } ] } }, { "id": "step-9", "name": "출근하기 (미출근 상태인 경우)", "description": "출근하기 버튼이 활성화된 경우 클릭하여 출근 기록", "condition": { "if": "{attendanceStatus} == 'not_checked_in'" }, "actions": [ { "type": "click", "target": "출근하기" } ], "waitFor": { "type": "text", "content": "출근 완료", "timeout": 5000 }, "expect": { "toast": ["출근", "완료", "성공"], "visible": ["출근 완료", "출근 시간"] } }, { "id": "step-10", "name": "출근 완료 상태 확인", "description": "출근 완료 후 상태 및 출근 시간 표시 확인", "verify": { "visible": ["출근 완료"], "checkInTime": { "format": "HH:mm:ss", "displayed": true }, "buttonState": { "출근하기": "hidden_or_disabled", "퇴근하기": "enabled_or_visible" } } }, { "id": "step-11", "name": "퇴근하기 버튼 상태 확인", "description": "출근 완료 후 퇴근하기 버튼 활성화 여부 확인", "verify": { "button": { "target": "퇴근하기", "state": "visible", "note": "일부 시스템에서는 최소 근무 시간 후에만 활성화될 수 있음" } } }, { "id": "step-12", "name": "퇴근하기 (선택적)", "description": "퇴근하기 버튼이 활성화된 경우 클릭하여 퇴근 기록", "optional": true, "condition": { "if": "button[name='퇴근하기']:enabled" }, "actions": [ { "type": "click", "target": "퇴근하기" } ], "waitFor": { "type": "text", "content": ["퇴근 완료", "퇴근 시간"], "timeout": 5000 }, "expect": { "toast": ["퇴근", "완료", "성공"], "visible": ["퇴근 완료", "퇴근 시간"] } }, { "id": "step-13", "name": "최종 상태 확인", "description": "출퇴근 기록 후 최종 상태 확인", "verify": { "url": "/hr/attendance", "mapDisplayed": true, "attendanceRecorded": true } } ], "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": "서울 영등포구 좌표 (테스트 회사 위치 가정)" } } }