{ "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": ["근태현황", "근태 현황", "출퇴근", "인사관리"] }, "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"], "login": { "url": "https://dev.codebridge-x.com/login", "credentials": { "id": "TestUser5", "password": "password123!" }, "successIndicator": "대시보드" }, "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": "🔐 위치 권한 사전 설정 (Playwright)", "description": "Playwright 컨텍스트 레벨에서 위치 권한을 미리 허용하여 브라우저 팝업 방지", "critical": true, "executeBeforeNavigation": true, "playwright": { "commands": [ { "tool": "mcp__playwright__playwright_evaluate", "script": "// 브라우저 시작 시 위치 권한 자동 허용 (Playwright 컨텍스트 레벨)" } ], "contextSetup": { "permissions": ["geolocation"], "geolocation": { "latitude": 37.557358, "longitude": 126.864414 } } }, "note": "이 단계는 브라우저 컨텍스트 생성 시 자동으로 처리됨" }, { "id": "step-0-1", "name": "🗺️ 위치 권한 팝업 처리 및 사이드바 메뉴 전체 펼치기", "description": "위치 권한 요청 팝업 처리 후 모두 펼치기 버튼을 클릭하여 전체 메뉴 펼침", "critical": true, "actions": [ { "type": "wait", "duration": 1000, "description": "페이지 로드 및 권한 팝업 표시 대기" }, { "type": "conditionalClick", "description": "위치 권한 팝업이 나타나면 '허용' 또는 '항상 허용' 버튼 클릭", "selectors": [ "button:has-text('항상 허용')", "button:has-text('허용')", "button:has-text('Allow')", "button:has-text('Always Allow')", "[data-testid='allow-location']", ".permission-allow-button", "button[aria-label*='허용']", "button[aria-label*='위치']" ], "fallback": { "action": "skip", "reason": "권한 팝업이 없으면 이미 허용된 상태" } }, { "type": "wait", "duration": 500, "description": "권한 설정 적용 대기" }, { "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": [ "위치 권한 팝업이 닫혔는지 확인", "또는 팝업이 처음부터 없었는지 확인 (이미 허용된 경우)" ], "errorHandling": { "onTimeout": "continue", "onNotFound": "continue", "reason": "팝업이 없으면 권한이 이미 허용된 상태로 간주" } }, { "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": "서울 영등포구 좌표 (테스트 회사 위치 가정)" } } }