refactor: 초정밀 시나리오 강화 (1060→1381 steps, 68/68 PASS)

This commit is contained in:
김보곤
2026-02-09 15:05:03 +09:00
parent 15ad1d9758
commit f5dffe2ee7
135 changed files with 23040 additions and 1652 deletions

View File

@@ -0,0 +1,461 @@
{
"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를 모킹하여 브라우저 권한 팝업이 나타나지 않도록 함",
"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 브라우저 좌측 상단에 나타나는 '사이트에 있는 동안 허용' 팝업 클릭",
"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_if_exists", "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_if_exists", "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_if_exists", "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_if_exists", "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": "서울 영등포구 좌표 (테스트 회사 위치 가정)"
}
}
}