- 54개 시나리오 파일에 URL 기반 메뉴 탐색을 위한 navigation 속성 추가 - targetUrl: 정확한 페이지 URL 경로 - urlPattern: ko 버전 포함 URL 패턴 (regex) - menuHints: 메뉴명 힌트 배열 (fallback용) 메뉴 탐색 실패율 41.8% → URL 기반 방식으로 개선 예정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
474 lines
16 KiB
JSON
474 lines
16 KiB
JSON
{
|
|
"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": "서울 영등포구 좌표 (테스트 회사 위치 가정)"
|
|
}
|
|
}
|
|
}
|