refactor: 비표준 포맷 13개 시나리오 Format A 통일
- actions 배열(Format B) → 단일 action(Format A) 변환 - fill_form fields: target 키 → name 키 수정 - verify_detail checks: 객체 배열 → 문자열 배열 수정 - 전체 13개 시나리오 E2E 테스트 PASS 확인
This commit is contained in:
@@ -104,20 +104,8 @@
|
||||
"id": 8,
|
||||
"phase": "FILTER",
|
||||
"name": "[FILTER] 기간 필터 적용",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[type='date']:first-of-type, input[placeholder*='시작'], input[name*='start']"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[type='date']:last-of-type, input[placeholder*='종료'], input[name*='end']"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const s = document.querySelector(\"input[type='date']:first-of-type, input[placeholder*='시작'], input[name*='start']\"); if (s) s.click(); await new Promise(r => setTimeout(r, 300)); const e = document.querySelector(\"input[type='date']:last-of-type, input[placeholder*='종료'], input[name*='end']\"); if (e) e.click(); await new Promise(r => setTimeout(r, 500)); return 'Filter inputs clicked'; })()",
|
||||
"expected": {
|
||||
"filter_applied": true
|
||||
}
|
||||
|
||||
@@ -66,242 +66,55 @@
|
||||
"id": 1,
|
||||
"name": "사이드바 메뉴 전체 펼치기",
|
||||
"description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scroll",
|
||||
"target": "sidebar",
|
||||
"direction": "top",
|
||||
"description": "사이드바 최상단으로 스크롤"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 2000
|
||||
}
|
||||
],
|
||||
"verification": [
|
||||
"사이드바가 화면에 보이는지 확인",
|
||||
"모든 메뉴가 펼쳐졌는지 확인"
|
||||
]
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const sidebar = document.querySelector('.sidebar-scroll, [class*=\"sidebar\"], nav'); if (sidebar) sidebar.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": 2,
|
||||
"name": "1차 메뉴 찾기: 결재관리 (스크롤 포함)",
|
||||
"description": "사이드바를 스크롤하며 '결재관리' 메뉴를 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "결재관리",
|
||||
"alternativeTexts": [
|
||||
"결재관리",
|
||||
"결재 관리",
|
||||
"Approval",
|
||||
"전자결재"
|
||||
],
|
||||
"scrollContainer": "sidebar",
|
||||
"maxAttempts": 10,
|
||||
"description": "스크롤하며 결재관리 메뉴 찾기"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "결재관리",
|
||||
"description": "결재관리 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500,
|
||||
"description": "서브메뉴 펼쳐지기 대기"
|
||||
},
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "approval_menu_expanded"
|
||||
}
|
||||
],
|
||||
"verification": [
|
||||
"결재관리 메뉴가 클릭되었는지 확인",
|
||||
"서브메뉴가 펼쳐졌는지 확인",
|
||||
"하위 메뉴 항목들이 보이는지 확인"
|
||||
],
|
||||
"fallback": {
|
||||
"if": "메뉴를 찾을 수 없음",
|
||||
"then": "사이드바 전체를 스크롤하며 재탐색"
|
||||
}
|
||||
"name": "결재관리 > 결재함 메뉴 진입",
|
||||
"description": "사이드바를 스크롤하며 '결재관리' > '결재함' 메뉴를 찾아 클릭",
|
||||
"action": "menu_navigate",
|
||||
"level1": "결재관리",
|
||||
"level2": "결재함"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "2차 메뉴 찾기: 결재함 (스크롤 포함)",
|
||||
"description": "서브메뉴에서 '결재함'을 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "결재함",
|
||||
"alternativeTexts": [
|
||||
"결재함",
|
||||
"결재 함",
|
||||
"Inbox",
|
||||
"승인함"
|
||||
],
|
||||
"scrollContainer": "submenu",
|
||||
"maxAttempts": 5,
|
||||
"description": "서브메뉴에서 결재함 찾기"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 200
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "결재함",
|
||||
"description": "결재함 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "페이지 로드 완료",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "approval_box_page"
|
||||
}
|
||||
],
|
||||
"verification": [
|
||||
"결재함 메뉴 클릭 성공",
|
||||
"페이지 이동 또는 컨텐츠 로드"
|
||||
]
|
||||
"name": "메뉴 도착 확인",
|
||||
"description": "결재함 페이지에 도착했는지 확인",
|
||||
"action": "verify_url",
|
||||
"target": "/approval/inbox"
|
||||
},
|
||||
{
|
||||
"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/approval/inbox",
|
||||
"/ko/approvals/inbox",
|
||||
"/ko/approval-box"
|
||||
],
|
||||
"stopOnSuccess": true
|
||||
},
|
||||
{
|
||||
"type": "ifStillFailed",
|
||||
"action": "navigateViaMenuClick",
|
||||
"description": "URL 직접 접근 실패 시 메뉴 클릭으로 재시도"
|
||||
}
|
||||
]
|
||||
}
|
||||
"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": 5,
|
||||
"name": "페이지 정상 로드 확인",
|
||||
"description": "결재함 페이지가 정상적으로 로드되었는지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "pageTitle",
|
||||
"contains": [
|
||||
"결재함",
|
||||
"결재",
|
||||
"Approval"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "pageContent",
|
||||
"notContains": [
|
||||
"404",
|
||||
"찾을 수 없습니다",
|
||||
"Not Found"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verification": [
|
||||
"페이지 제목 '결재함' 또는 관련 텍스트 표시",
|
||||
"404 에러 메시지 미표시",
|
||||
"콘텐츠가 정상 렌더링됨"
|
||||
],
|
||||
"successCriteria": {
|
||||
"urlPattern": "/approval",
|
||||
"requiredElements": [
|
||||
"결재",
|
||||
"문서",
|
||||
"승인"
|
||||
]
|
||||
}
|
||||
"action": "evaluate",
|
||||
"script": "(() => { const bodyText = document.body.innerText || ''; const titleCheck = ['결재함', '결재', 'Approval'].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": 6,
|
||||
"name": "통계 카드 확인",
|
||||
"action": "verify_element",
|
||||
"target": "[class*='card'], [class*='stat']",
|
||||
"verification": [
|
||||
"전체결재 건수 기록",
|
||||
"미결재 건수 기록",
|
||||
"결재완료 건수 기록",
|
||||
"결재반려 건수 기록"
|
||||
]
|
||||
"target": "[class*='card'], [class*='stat']"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "탭 구조 확인",
|
||||
"action": "verify_element",
|
||||
"target": "[role='tab'], button[role='tab']",
|
||||
"verification": [
|
||||
"'전체결재' 탭 존재 확인",
|
||||
"'미결재' 탭 존재 확인",
|
||||
"'결재완료' 탭 존재 확인",
|
||||
"'결재반려' 탭 존재 확인"
|
||||
]
|
||||
"target": "[role='tab'], button[role='tab']"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "테이블 데이터 확인",
|
||||
"action": "verify_table",
|
||||
"target": "table",
|
||||
"verification": [
|
||||
"테이블 헤더 컬럼 확인",
|
||||
"데이터 행 존재 여부 확인",
|
||||
"페이지네이션 표시 확인"
|
||||
]
|
||||
"target": "table"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
@@ -311,139 +124,39 @@
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "⚠️ 필수 검증: 결재 문서 상세 보기",
|
||||
"name": "필수 검증: 결재 문서 상세 보기",
|
||||
"description": "테이블에서 결재 문서 클릭하여 상세 모달/페이지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "미결재 탭",
|
||||
"description": "미결재 탭으로 이동"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "첫 번째 결재 문서 행",
|
||||
"description": "결재 문서 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "상세 모달 또는 페이지"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"detailView": true,
|
||||
"fields": [
|
||||
"문서 제목",
|
||||
"기안자",
|
||||
"기안일",
|
||||
"결재 상태"
|
||||
],
|
||||
"buttons": [
|
||||
"승인",
|
||||
"반려",
|
||||
"PDF",
|
||||
"인쇄"
|
||||
]
|
||||
},
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('미결재')); if (tab) { tab.click(); await new Promise(r => setTimeout(r, 500)); } const row = document.querySelector('table tbody tr'); if (row) { row.click(); await new Promise(r => setTimeout(r, 1000)); } const bodyText = document.body.innerText || ''; const hasDetail = ['문서 제목', '기안자', '기안일', '결재 상태', '승인', '반려'].some(t => bodyText.includes(t)); return hasDetail ? 'PASS: Detail view opened' : 'WARN: Detail view not confirmed'; })()",
|
||||
"note": "결재 문서가 없으면 데이터 생성 또는 SKIP"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "⚠️ 필수 검증: PDF 다운로드 전 모달 스크린샷",
|
||||
"description": "PDF 생성 전 모달 상태를 스크린샷으로 캡처하여 CSS 문제 감지용 기준 이미지 확보",
|
||||
"prerequisite": "step-8의 문서 상세 모달이 열려있는 상태에서 실행",
|
||||
"actions": [
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "pdf-preview-before-download-approval-box",
|
||||
"fullPage": false,
|
||||
"selector": "[role='dialog'], .modal, [data-state='open']",
|
||||
"savePath": "tests/e2e/results/hotfix/screenshots/",
|
||||
"description": "PDF 생성 대상 모달 전체 캡처"
|
||||
}
|
||||
],
|
||||
"verify": {
|
||||
"screenshotCaptured": true,
|
||||
"purpose": "PDF CSS 문제 감지를 위한 기준 이미지"
|
||||
}
|
||||
"name": "PDF 다운로드 전 모달 상태 확인",
|
||||
"description": "PDF 생성 전 모달 상태를 확인하여 CSS 문제 감지용 기준 확보",
|
||||
"action": "evaluate",
|
||||
"script": "(() => { const modal = document.querySelector(\"[role='dialog'], .modal, [data-state='open']\"); if (modal && modal.offsetParent !== null) { return 'PASS: Modal is open for PDF preview'; } return 'WARN: No modal open for PDF preview'; })()"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "⚠️ 필수 검증: PDF 다운로드 실행 및 파일 보관",
|
||||
"description": "PDF 다운로드 후 파일을 지정 폴더에 보관하여 수동 검증 가능하게 함",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "PDF 버튼 존재",
|
||||
"selector": "button:has-text('PDF'), [aria-label*='PDF']",
|
||||
"description": "PDF 다운로드 버튼 존재 확인"
|
||||
},
|
||||
{
|
||||
"type": "expectResponse",
|
||||
"id": "pdf-download-response-approval-box",
|
||||
"urlPattern": "/api/v1/approvals/*/pdf",
|
||||
"description": "PDF 다운로드 API 응답 대기 설정"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "PDF 버튼",
|
||||
"selector": "button:has-text('PDF')",
|
||||
"description": "PDF 다운로드 버튼 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 3000,
|
||||
"description": "PDF 생성 및 다운로드 대기"
|
||||
},
|
||||
{
|
||||
"type": "assertResponse",
|
||||
"id": "pdf-download-response-approval-box",
|
||||
"checks": {
|
||||
"status": 200,
|
||||
"contentType": "application/pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "saveDownloadedFile",
|
||||
"targetPath": "tests/e2e/results/hotfix/pdf-samples/",
|
||||
"fileNamePattern": "approval-box-{timestamp}.pdf",
|
||||
"description": "다운로드된 PDF 파일을 지정 폴더에 보관"
|
||||
}
|
||||
],
|
||||
"verify": {
|
||||
"apiSuccess": true,
|
||||
"fileDownloaded": true,
|
||||
"fileSaved": "tests/e2e/results/hotfix/pdf-samples/"
|
||||
}
|
||||
"name": "필수 검증: PDF 다운로드 실행",
|
||||
"description": "PDF 다운로드 버튼 클릭 및 API 응답 확인",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const pdfBtn = document.querySelector(\"button:has-text('PDF'), [aria-label*='PDF']\") || Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('PDF')); if (!pdfBtn) return 'WARN: PDF button not found'; pdfBtn.click(); await new Promise(r => setTimeout(r, 3000)); const logs = window.__API_LOGS__ || []; const pdfCall = logs.find(l => l.url?.includes('/pdf')); if (pdfCall && pdfCall.ok) return 'PASS: PDF download API success (status ' + pdfCall.status + ')'; if (pdfCall) return 'FAIL: PDF API error (status ' + pdfCall.status + ')'; return 'WARN: PDF API call not captured'; })()"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "⚠️ PDF 파일 유효성 검증",
|
||||
"name": "PDF 파일 유효성 검증",
|
||||
"description": "다운로드된 PDF 파일의 기본 유효성 검사",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verifyDownloadedFile",
|
||||
"checks": {
|
||||
"fileExists": true,
|
||||
"fileSize": "> 1024",
|
||||
"pdfSignature": "%PDF-",
|
||||
"description": "PDF 파일 헤더 검증"
|
||||
}
|
||||
}
|
||||
],
|
||||
"verify": {
|
||||
"pdfValid": true,
|
||||
"minFileSize": "1KB 이상"
|
||||
}
|
||||
"action": "evaluate",
|
||||
"script": "(() => { const logs = window.__API_LOGS__ || []; const pdfCall = logs.find(l => l.url?.includes('/pdf')); if (pdfCall && pdfCall.ok && pdfCall.status === 200) return 'PASS: PDF API returned 200'; return 'WARN: PDF validity could not be confirmed via API logs'; })()"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "📋 PDF 스타일 수동 확인 체크리스트",
|
||||
"type": "manualVerification",
|
||||
"name": "PDF 스타일 수동 확인 체크리스트",
|
||||
"description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목",
|
||||
"action": "evaluate",
|
||||
"script": "(() => { return 'Manual check items: 테이블 경계선, 한글 폰트, 숫자 정렬, 여백, 헤더/푸터, 로고/이미지, 페이지 나눔, 배경색, 텍스트 오버플로우, 결재선 표시'; })()",
|
||||
"manualChecklist": [
|
||||
{
|
||||
"id": "css-1",
|
||||
@@ -495,63 +208,22 @@
|
||||
"item": "결재선 정보가 정상적으로 표시되는가?",
|
||||
"category": "결재선"
|
||||
}
|
||||
],
|
||||
"outputFiles": {
|
||||
"screenshot": "tests/e2e/results/hotfix/screenshots/pdf-preview-before-download-approval-box-*.png",
|
||||
"pdfFile": "tests/e2e/results/hotfix/pdf-samples/approval-box-*.pdf"
|
||||
},
|
||||
"reportFlag": {
|
||||
"requiresManualReview": true,
|
||||
"message": "⚠️ PDF 스타일 수동 확인 필요 - 위 체크리스트 항목을 PDF 파일에서 직접 확인하세요"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "⚠️ 필수 검증 #4: 결재 승인 실제 수행",
|
||||
"name": "필수 검증: 결재 승인 실제 수행",
|
||||
"description": "미결재 문서에 대해 실제 승인 처리 수행",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "승인 버튼 존재"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "승인 버튼",
|
||||
"description": "결재 승인 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "확인 다이얼로그"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "확인",
|
||||
"description": "승인 확인"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"urlMaintained": true,
|
||||
"noErrorPage": true,
|
||||
"apiCall": "POST /api/v1/approvals/{id}/approve",
|
||||
"toast": "승인되었습니다",
|
||||
"statusChange": "미결재 → 결재완료"
|
||||
},
|
||||
"note": "⚠️ 버튼 존재만 확인하면 불완전! 실제 승인까지 검증 필수!"
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const approveBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('승인')); if (!approveBtn) return 'WARN: Approve button not found'; approveBtn.click(); await new Promise(r => setTimeout(r, 500)); const confirmBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('확인')); if (confirmBtn) { confirmBtn.click(); await new Promise(r => setTimeout(r, 1000)); } return 'Approval action attempted'; })()",
|
||||
"note": "버튼 존재만 확인하면 불완전! 실제 승인까지 검증 필수!"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "결재 승인 결과 확인",
|
||||
"description": "승인 후 결재완료 탭에서 해당 문서 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "결재완료 탭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('결재완료')); if (tab) { tab.click(); await new Promise(r => setTimeout(r, 500)); } return 'Switched to completed tab'; })()",
|
||||
"verify": {
|
||||
"documentMoved": "승인한 문서가 결재완료 탭에 표시",
|
||||
"statusUpdated": "결재 상태가 '완료'로 변경"
|
||||
@@ -559,69 +231,18 @@
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "⚠️ 필수 검증 #4: 결재 반려 실제 수행",
|
||||
"name": "필수 검증: 결재 반려 실제 수행",
|
||||
"description": "미결재 문서에 대해 실제 반려 처리 수행",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "미결재 탭",
|
||||
"description": "미결재 탭으로 이동"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "결재 문서 행",
|
||||
"description": "결재 문서 선택"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "상세 보기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "반려 버튼",
|
||||
"description": "결재 반려 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "반려 사유 입력 모달"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "반려 사유"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "확인",
|
||||
"description": "반려 확인"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"urlMaintained": true,
|
||||
"noErrorPage": true,
|
||||
"apiCall": "POST /api/v1/approvals/{id}/reject",
|
||||
"toast": "반려되었습니다",
|
||||
"statusChange": "미결재 → 결재반려"
|
||||
},
|
||||
"note": "⚠️ 반려 버튼 존재만 확인하면 불완전! 실제 반려까지 검증 필수!"
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const pendingTab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('미결재')); if (pendingTab) { pendingTab.click(); await new Promise(r => setTimeout(r, 500)); } const row = document.querySelector('table tbody tr'); if (row) { row.click(); await new Promise(r => setTimeout(r, 500)); } const rejectBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('반려')); if (!rejectBtn) return 'WARN: Reject button not found'; rejectBtn.click(); await new Promise(r => setTimeout(r, 500)); const reasonField = Array.from(document.querySelectorAll('button, input, textarea')).find(e => e.placeholder?.includes('사유') || e.innerText?.includes('반려 사유')); if (reasonField) reasonField.click(); await new Promise(r => setTimeout(r, 300)); const confirmBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('확인')); if (confirmBtn) { confirmBtn.click(); await new Promise(r => setTimeout(r, 1000)); } return 'Rejection action attempted'; })()",
|
||||
"note": "반려 버튼 존재만 확인하면 불완전! 실제 반려까지 검증 필수!"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "결재 반려 결과 확인",
|
||||
"description": "반려 후 결재반려 탭에서 해당 문서 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "결재반려 탭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const tab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('결재반려')); if (tab) { tab.click(); await new Promise(r => setTimeout(r, 500)); } return 'Switched to rejected tab'; })()",
|
||||
"verify": {
|
||||
"documentMoved": "반려한 문서가 결재반려 탭에 표시",
|
||||
"statusUpdated": "결재 상태가 '반려'로 변경",
|
||||
@@ -632,24 +253,8 @@
|
||||
"id": 19,
|
||||
"name": "검색 기능 테스트",
|
||||
"description": "검색 필터로 결재 문서 검색",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "전체결재 탭"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "검색 입력창"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "검색 버튼"
|
||||
}
|
||||
],
|
||||
"verify": {
|
||||
"searchApplied": true,
|
||||
"filteredResults": "검색어에 맞는 결과 표시"
|
||||
}
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const allTab = Array.from(document.querySelectorAll('[role=tab], button')).find(b => b.innerText?.includes('전체결재')); if (allTab) { allTab.click(); await new Promise(r => setTimeout(r, 300)); } const searchInput = document.querySelector('input[type=search], input[placeholder*=검색]'); if (searchInput) { searchInput.click(); await new Promise(r => setTimeout(r, 200)); } const searchBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('검색')); if (searchBtn) { searchBtn.click(); await new Promise(r => setTimeout(r, 500)); } return 'Search test completed'; })()"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
@@ -682,9 +287,9 @@
|
||||
"POST /api/v1/approvals/{id}/reject - 결재 반려"
|
||||
],
|
||||
"notes": [
|
||||
"⚠️ 404 방지: 반드시 메뉴 클릭으로 페이지 진입 (직접 URL 접근 금지)",
|
||||
"⚠️ 스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음",
|
||||
"⚠️ 대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색",
|
||||
"404 방지: 반드시 메뉴 클릭으로 페이지 진입 (직접 URL 접근 금지)",
|
||||
"스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음",
|
||||
"대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색",
|
||||
"메뉴 계층: 결재관리 > 결재함",
|
||||
"탭 전환 시 URL 변경 없이 데이터만 필터링됨"
|
||||
]
|
||||
|
||||
@@ -124,45 +124,18 @@
|
||||
"steps": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "🔐 Geolocation API 모킹 (권한 팝업 방지)",
|
||||
"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": "모킹 적용 대기"
|
||||
}
|
||||
],
|
||||
"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": "🗺️ 브라우저 위치 권한 팝업 클릭 (좌측 상단)",
|
||||
"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": "권한 설정 적용 대기"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
@@ -171,253 +144,65 @@
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "📂 사이드바 메뉴 전체 펼치기",
|
||||
"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": [
|
||||
"사이드바 메뉴가 펼쳐졌는지 확인"
|
||||
]
|
||||
"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": "사이드바를 스크롤하며 '인사관리' 메뉴를 찾아 클릭",
|
||||
"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": "사이드바 전체를 스크롤하며 재탐색"
|
||||
}
|
||||
"action": "menu_navigate",
|
||||
"level1": "인사관리",
|
||||
"level2": "근태현황"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"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": [
|
||||
"근태현황 메뉴 클릭 성공",
|
||||
"페이지 이동 또는 컨텐츠 로드"
|
||||
]
|
||||
"name": "2차 메뉴 도착 확인",
|
||||
"description": "근태현황 페이지에 도착했는지 확인",
|
||||
"action": "verify_url",
|
||||
"target": "/hr/attendance"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"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 직접 접근 실패 시 메뉴 클릭으로 재시도"
|
||||
}
|
||||
]
|
||||
}
|
||||
"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": "근태현황 페이지가 정상적으로 로드되었는지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "pageTitle",
|
||||
"contains": [
|
||||
"근태현황",
|
||||
"출퇴근",
|
||||
"Attendance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "pageContent",
|
||||
"notContains": [
|
||||
"404",
|
||||
"찾을 수 없습니다",
|
||||
"Not Found"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verification": [
|
||||
"페이지 제목 '근태현황' 또는 관련 텍스트 표시",
|
||||
"404 에러 메시지 미표시",
|
||||
"콘텐츠가 정상 렌더링됨"
|
||||
],
|
||||
"successCriteria": {
|
||||
"urlPattern": "/hr/attendance",
|
||||
"requiredElements": [
|
||||
"출퇴근",
|
||||
"출근",
|
||||
"퇴근",
|
||||
"현재 시간"
|
||||
]
|
||||
}
|
||||
"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에서 위치 정보 권한을 허용하고 가상 위치 설정",
|
||||
"playwright": {
|
||||
"code": "await context.grantPermissions(['geolocation']);",
|
||||
"setGeolocation": {
|
||||
"latitude": 37.557358,
|
||||
"longitude": 126.864414
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"permissionGranted": "geolocation"
|
||||
}
|
||||
"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 로딩 및 현재 위치 표시 대기",
|
||||
"waitFor": {
|
||||
"type": "element",
|
||||
"selector": "region[name='지도']",
|
||||
"action": "wait_for_element",
|
||||
"target": "region[name='지도'], [class*='map'], canvas, iframe[src*='map']",
|
||||
"timeout": 10000
|
||||
},
|
||||
"expect": {
|
||||
"mapLoaded": true,
|
||||
"locationMarkerVisible": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "사용자 정보 확인",
|
||||
"description": "출퇴근 패널에서 로그인한 사용자 정보 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"verify": {
|
||||
"userInfo": {
|
||||
"name": "홍킬동",
|
||||
"department": "부서명 · 개발중인 메뉴"
|
||||
},
|
||||
"currentTime": {
|
||||
"format": "HH:mm:ss",
|
||||
"updating": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -425,66 +210,29 @@
|
||||
"id": 11,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
"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'"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "출근하기"
|
||||
}
|
||||
],
|
||||
"waitFor": {
|
||||
"type": "text",
|
||||
"content": "출근 완료",
|
||||
"timeout": 5000
|
||||
},
|
||||
"expect": {
|
||||
"toast": [
|
||||
"출근",
|
||||
"완료",
|
||||
"성공"
|
||||
],
|
||||
"visible": [
|
||||
"출근 완료",
|
||||
"출근 시간"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "출근 완료 상태 확인",
|
||||
"description": "출근 완료 후 상태 및 출근 시간 표시 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"verify": {
|
||||
"visible": [
|
||||
"출근 완료"
|
||||
],
|
||||
"checkInTime": {
|
||||
"format": "HH:mm:ss",
|
||||
"displayed": true
|
||||
},
|
||||
"buttonState": {
|
||||
"출근하기": "hidden_or_disabled",
|
||||
"퇴근하기": "enabled_or_visible"
|
||||
@@ -495,6 +243,8 @@
|
||||
"id": 14,
|
||||
"name": "퇴근하기 버튼 상태 확인",
|
||||
"description": "출근 완료 후 퇴근하기 버튼 활성화 여부 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"verify": {
|
||||
"button": {
|
||||
"target": "퇴근하기",
|
||||
@@ -508,39 +258,18 @@
|
||||
"name": "퇴근하기 (선택적)",
|
||||
"description": "퇴근하기 버튼이 활성화된 경우 클릭하여 퇴근 기록",
|
||||
"optional": true,
|
||||
"action": "click_if_exists",
|
||||
"target": "퇴근하기",
|
||||
"condition": {
|
||||
"if": "button[name='퇴근하기']:enabled"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "퇴근하기"
|
||||
}
|
||||
],
|
||||
"waitFor": {
|
||||
"type": "text",
|
||||
"content": [
|
||||
"퇴근 완료",
|
||||
"퇴근 시간"
|
||||
],
|
||||
"timeout": 5000
|
||||
},
|
||||
"expect": {
|
||||
"toast": [
|
||||
"퇴근",
|
||||
"완료",
|
||||
"성공"
|
||||
],
|
||||
"visible": [
|
||||
"퇴근 완료",
|
||||
"퇴근 시간"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "최종 상태 확인",
|
||||
"description": "출퇴근 기록 후 최종 상태 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"verify": {
|
||||
"url": "/hr/attendance",
|
||||
"mapDisplayed": true,
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
"id": 19,
|
||||
"name": "[회계관리 > 입금관리] [DELETE #1] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_1'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_입금'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.deleted=!document.body.innerText?.includes(R.targetText?.substring(0,20));R.ok=R.deleted!==false;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_1'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_입금'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -186,7 +186,7 @@
|
||||
"id": 23,
|
||||
"name": "[회계관리 > 입금관리] [DELETE #2] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_2'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_입금'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.deleted=!document.body.innerText?.includes(R.targetText?.substring(0,20));R.ok=R.deleted!==false;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_2'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_입금'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -232,7 +232,7 @@
|
||||
"id": 27,
|
||||
"name": "[회계관리 > 입금관리] [DELETE #3] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_3'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_입금'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.deleted=!document.body.innerText?.includes(R.targetText?.substring(0,20));R.ok=R.deleted!==false;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_3'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_입금'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
"id": 19,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #1] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_1'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_BATCH_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_BATCH_'));if(!targetRow){R.error='E2E_BATCH_+ts 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.deleted=!document.body.innerText?.includes(R.targetText?.substring(0,20));R.ok=R.deleted!==false;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_1'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_BATCH_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_BATCH_'));if(!targetRow){R.error='E2E_BATCH_+ts 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -165,11 +165,28 @@
|
||||
"action": "wait",
|
||||
"timeout": 1500
|
||||
},
|
||||
{
|
||||
"id": 201,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #2 준비] 페이지 새로고침",
|
||||
"action": "reload"
|
||||
},
|
||||
{
|
||||
"id": 202,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #2 준비] 새로고침 대기",
|
||||
"action": "wait",
|
||||
"timeout": 1000
|
||||
},
|
||||
{
|
||||
"id": 203,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #2 준비] 테이블 로드 대기",
|
||||
"action": "wait_for_table",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #2] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_2'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_BATCH_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_BATCH_'));if(!targetRow){R.error='E2E_BATCH_+ts 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.deleted=!document.body.innerText?.includes(R.targetText?.substring(0,20));R.ok=R.deleted!==false;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_2'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_BATCH_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_BATCH_'));if(!targetRow){R.error='E2E_BATCH_+ts 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -194,11 +211,28 @@
|
||||
"action": "wait",
|
||||
"timeout": 1500
|
||||
},
|
||||
{
|
||||
"id": 204,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #3 준비] 페이지 새로고침",
|
||||
"action": "reload"
|
||||
},
|
||||
{
|
||||
"id": 205,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #3 준비] 새로고침 대기",
|
||||
"action": "wait",
|
||||
"timeout": 1000
|
||||
},
|
||||
{
|
||||
"id": 206,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #3 준비] 테이블 로드 대기",
|
||||
"action": "wait_for_table",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "[게시판 > 자유게시판] [DELETE #3] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_3'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_BATCH_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_BATCH_'));if(!targetRow){R.error='E2E_BATCH_+ts 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.deleted=!document.body.innerText?.includes(R.targetText?.substring(0,20));R.ok=R.deleted!==false;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE_3'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes('E2E_BATCH_')&&r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_BATCH_'));if(!targetRow){R.error='E2E_BATCH_+ts 데이터 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
|
||||
@@ -89,26 +89,8 @@
|
||||
"id": 1,
|
||||
"name": "사이드바 메뉴 전체 펼치기",
|
||||
"description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scroll",
|
||||
"target": "sidebar",
|
||||
"direction": "top",
|
||||
"description": "사이드바 최상단으로 스크롤"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 2000
|
||||
}
|
||||
],
|
||||
"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'; })()",
|
||||
"verification": [
|
||||
"사이드바가 화면에 보이는지 확인",
|
||||
"모든 메뉴가 펼쳐졌는지 확인"
|
||||
@@ -116,41 +98,11 @@
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "1차 메뉴 찾기: 설정 (스크롤 포함)",
|
||||
"name": "1차 메뉴 찾기: 설정",
|
||||
"description": "사이드바를 스크롤하며 '설정' 메뉴를 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "설정",
|
||||
"alternativeTexts": [
|
||||
"설정",
|
||||
"Settings",
|
||||
"환경설정",
|
||||
"시스템설정"
|
||||
],
|
||||
"scrollContainer": "sidebar",
|
||||
"maxAttempts": 10,
|
||||
"description": "스크롤하며 설정 메뉴 찾기"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "설정",
|
||||
"description": "설정 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500,
|
||||
"description": "서브메뉴 펼쳐지기 대기"
|
||||
},
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "settings_menu_expanded"
|
||||
}
|
||||
],
|
||||
"action": "menu_navigate",
|
||||
"level1": "설정",
|
||||
"level2": "회사정보",
|
||||
"verification": [
|
||||
"설정 메뉴가 클릭되었는지 확인",
|
||||
"서브메뉴가 펼쳐졌는지 확인",
|
||||
@@ -163,41 +115,9 @@
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "2차 메뉴 찾기: 회사정보 (스크롤 포함)",
|
||||
"description": "서브메뉴에서 '회사정보'를 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "회사정보",
|
||||
"alternativeTexts": [
|
||||
"회사정보",
|
||||
"회사 정보",
|
||||
"Company Info",
|
||||
"회사관리"
|
||||
],
|
||||
"scrollContainer": "submenu",
|
||||
"maxAttempts": 5,
|
||||
"description": "서브메뉴에서 회사정보 찾기"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 200
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "회사정보",
|
||||
"description": "회사정보 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "페이지 로드 완료",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "company_info_page"
|
||||
}
|
||||
],
|
||||
"name": "페이지 로드 대기",
|
||||
"action": "wait",
|
||||
"duration": 2000,
|
||||
"verification": [
|
||||
"회사정보 메뉴 클릭 성공",
|
||||
"페이지 이동 또는 컨텐츠 로드"
|
||||
@@ -205,50 +125,20 @@
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "404 에러 감지 및 대체 경로 시도",
|
||||
"description": "페이지 로드 후 404 에러 여부 확인, 404시 대체 경로 탐색",
|
||||
"actions": [
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 1000
|
||||
"name": "404 에러 감지",
|
||||
"description": "페이지 로드 후 404 에러 여부 확인",
|
||||
"action": "verify_url",
|
||||
"expected": {
|
||||
"no_404": true
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
"fallbackUrls": [
|
||||
"/company-info",
|
||||
"/ko/company-info"
|
||||
],
|
||||
"stopOnSuccess": true
|
||||
},
|
||||
{
|
||||
"type": "ifStillFailed",
|
||||
"action": "navigateViaMenuClick",
|
||||
"description": "URL 직접 접근 실패 시 메뉴 클릭으로 재시도"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -256,25 +146,12 @@
|
||||
"id": 5,
|
||||
"name": "페이지 정상 로드 확인",
|
||||
"description": "회사정보 페이지가 정상적으로 로드되었는지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "pageTitle",
|
||||
"contains": [
|
||||
"회사정보",
|
||||
"회사 정보",
|
||||
"Company"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "pageContent",
|
||||
"notContains": [
|
||||
"404",
|
||||
"찾을 수 없습니다",
|
||||
"Not Found"
|
||||
]
|
||||
}
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"visible_text:회사정보",
|
||||
"not_contains:404",
|
||||
"not_contains:찾을 수 없습니다",
|
||||
"not_contains:Not Found"
|
||||
],
|
||||
"verification": [
|
||||
"페이지 제목 '회사정보' 또는 관련 텍스트 표시",
|
||||
@@ -438,13 +315,8 @@
|
||||
"step": 21,
|
||||
"name": "수정 모드에서 데이터 변경 테스트",
|
||||
"description": "실제 데이터를 수정하고 저장 기능 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "수정",
|
||||
"description": "수정 모드 진입"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"fieldsEnabled": true
|
||||
},
|
||||
@@ -454,29 +326,17 @@
|
||||
"step": 22,
|
||||
"name": "업태 필드 수정",
|
||||
"description": "업태 필드 값 변경",
|
||||
"actions": [
|
||||
{
|
||||
"type": "clear",
|
||||
"target": "업태"
|
||||
},
|
||||
{
|
||||
"type": "fill",
|
||||
"action": "edit_field",
|
||||
"target": "업태",
|
||||
"value": "테스트업태_수정"
|
||||
}
|
||||
],
|
||||
"value": "테스트업태_수정",
|
||||
"id": 23
|
||||
},
|
||||
{
|
||||
"step": 23,
|
||||
"name": "저장 버튼 클릭",
|
||||
"description": "수정된 회사 정보 저장",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "저장"
|
||||
}
|
||||
],
|
||||
"action": "click_if_exists",
|
||||
"target": "저장",
|
||||
"waitFor": {
|
||||
"type": "apiResponse",
|
||||
"method": "PUT",
|
||||
@@ -497,6 +357,10 @@
|
||||
"name": "⚠️ 필수 검증: 수정 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!",
|
||||
"description": "수정된 업태 값이 반영되었는지 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"visible_text:테스트업태_수정"
|
||||
],
|
||||
"verify": {
|
||||
"fieldValue": {
|
||||
"target": "업태",
|
||||
@@ -509,12 +373,8 @@
|
||||
"step": 25,
|
||||
"name": "회사 추가 다이얼로그 열기",
|
||||
"description": "회사 추가 버튼 클릭하여 다이얼로그 열기",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "회사 추가"
|
||||
}
|
||||
],
|
||||
"action": "click_if_exists",
|
||||
"target": "회사 추가",
|
||||
"expect": {
|
||||
"dialog": true,
|
||||
"visible": [
|
||||
@@ -531,20 +391,18 @@
|
||||
"step": 26,
|
||||
"name": "새 회사 정보 입력",
|
||||
"description": "회사 추가 다이얼로그에서 필수 정보 입력",
|
||||
"actions": [
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{
|
||||
"type": "fill",
|
||||
"target": "회사명",
|
||||
"name": "회사명",
|
||||
"value": "테스트회사_{timestamp}"
|
||||
},
|
||||
{
|
||||
"type": "fill",
|
||||
"target": "대표자명",
|
||||
"name": "대표자명",
|
||||
"value": "테스트대표"
|
||||
},
|
||||
{
|
||||
"type": "fill",
|
||||
"target": "사업자등록번호",
|
||||
"name": "사업자등록번호",
|
||||
"value": "123-45-67890"
|
||||
}
|
||||
],
|
||||
@@ -554,12 +412,8 @@
|
||||
"step": 27,
|
||||
"name": "회사 등록",
|
||||
"description": "등록 버튼 클릭하여 새 회사 등록",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "등록"
|
||||
}
|
||||
],
|
||||
"action": "click_if_exists",
|
||||
"target": "등록",
|
||||
"waitFor": {
|
||||
"type": "apiResponse",
|
||||
"method": "POST",
|
||||
@@ -580,6 +434,8 @@
|
||||
"name": "⚠️ 필수 검증: 회사 등록 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 등록 확인 필수!",
|
||||
"description": "등록된 회사가 목록에 표시되는지 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"verify": {
|
||||
"visible": "테스트회사"
|
||||
},
|
||||
@@ -589,25 +445,8 @@
|
||||
"step": 29,
|
||||
"name": "원복: 업태 필드 원래 값으로 복구",
|
||||
"description": "테스트 후 원래 값으로 복구",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "수정"
|
||||
},
|
||||
{
|
||||
"type": "clear",
|
||||
"target": "업태"
|
||||
},
|
||||
{
|
||||
"type": "fill",
|
||||
"target": "업태",
|
||||
"value": "업태명"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "저장"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const clickBtn = (text) => { const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.trim() === text); if(btn) btn.click(); return !!btn; }; clickBtn('수정'); await new Promise(r=>setTimeout(r,1000)); const inputs = document.querySelectorAll('input:not([type=\"hidden\"])'); let target = null; inputs.forEach(inp => { const labels = document.querySelectorAll('label'); labels.forEach(lbl => { if(lbl.innerText?.includes('업태') && (lbl.htmlFor === inp.id || lbl.contains(inp))) target = inp; }); if(!target && inp.value === '테스트업태_수정') target = inp; }); if(target){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(target,''); target.dispatchEvent(new Event('input',{bubbles:true})); nset.call(target,'업태명'); target.dispatchEvent(new Event('input',{bubbles:true})); target.dispatchEvent(new Event('change',{bubbles:true})); } await new Promise(r=>setTimeout(r,500)); clickBtn('저장'); await new Promise(r=>setTimeout(r,2000)); return 'restored'; })()",
|
||||
"expect": {
|
||||
"toast": [
|
||||
"수정",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"id": 8,
|
||||
"name": "[게시판 > 자유게시판] [DELETE] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;const R={phase:'DELETE'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);R.ts=ts;targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(confirmBtn){confirmBtn.click();await w(3000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const targetRow=rows.find(r=>r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(!targetRow){R.error='E2E_TEST_ 데이터 없음';R.ok=false;return JSON.stringify(R);}R.targetText=targetRow.innerText?.substring(0,60);R.ts=ts;targetRow.click();await w(2500);R.detailUrl=location.pathname+location.search;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제'&&b.offsetParent!==null);if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1500);let confirmBtn=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!confirmBtn){confirmBtn=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!confirmBtn){confirmBtn=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(confirmBtn){confirmBtn.click();await w(4000);}R.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -91,11 +91,29 @@
|
||||
"action": "wait",
|
||||
"timeout": 2000
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 삭제 후 새로고침",
|
||||
"action": "reload",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 새로고침 대기",
|
||||
"action": "wait",
|
||||
"timeout": 3000
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 테이블 로드 대기",
|
||||
"action": "wait_for_table",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 삭제 확인",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;const R={phase:'VERIFY_DELETE'};await w(1000);R.url=location.pathname;R.ts=ts;const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;const found=Array.from(rows).find(r=>r.innerText?.includes(ts));R.stillExists=!!found;R.ok=!found;if(found)R.warn='E2E_TEST_ 데이터가 여전히 존재 - 수동 삭제 필요';return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{try{return sessionStorage.getItem('__E2E_TS__')}catch(e){return null}})()||'E2E_TEST_';const R={phase:'VERIFY_DELETE'};await w(1000);R.url=location.pathname;R.ts=ts;const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;const found=Array.from(rows).find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts));R.stillExists=!!found;R.ok=!found;if(found)R.warn='E2E_TEST_ 데이터가 여전히 존재 - 수동 삭제 필요';return JSON.stringify(R);})()",
|
||||
"timeout": 15000,
|
||||
"phase": "VERIFY"
|
||||
}
|
||||
|
||||
@@ -69,77 +69,16 @@
|
||||
"id": 1,
|
||||
"name": "사이드바 메뉴 전체 펼치기",
|
||||
"description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scroll",
|
||||
"target": "sidebar",
|
||||
"direction": "top",
|
||||
"description": "사이드바 최상단으로 스크롤"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 2000
|
||||
}
|
||||
]
|
||||
"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 'sidebar expanded';})()"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "2단계 메뉴 진입: 회계관리 > 입금관리",
|
||||
"description": "사이드바를 스크롤하며 회계관리 > 입금관리 메뉴를 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "회계관리",
|
||||
"alternativeTexts": [
|
||||
"회계관리",
|
||||
"회계 관리",
|
||||
"Accounting"
|
||||
],
|
||||
"scrollContainer": "sidebar",
|
||||
"maxAttempts": 10,
|
||||
"description": "스크롤하며 회계관리 메뉴 찾기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "회계관리",
|
||||
"description": "회계관리 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500,
|
||||
"description": "서브메뉴 펼쳐지기 대기"
|
||||
},
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "입금관리",
|
||||
"alternativeTexts": [
|
||||
"입금관리",
|
||||
"입금 관리",
|
||||
"Deposits"
|
||||
],
|
||||
"scrollContainer": "submenu",
|
||||
"maxAttempts": 5,
|
||||
"description": "서브메뉴에서 입금관리 찾기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "입금관리",
|
||||
"description": "입금관리 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "페이지 로드 완료",
|
||||
"timeout": 10000
|
||||
}
|
||||
],
|
||||
"action": "menu_navigate",
|
||||
"level1": "회계관리",
|
||||
"level2": "입금관리",
|
||||
"expect": {
|
||||
"url": "/accounting/deposits",
|
||||
"visible": [
|
||||
@@ -157,6 +96,8 @@
|
||||
"id": 3,
|
||||
"name": "목록 페이지 구조 확인",
|
||||
"description": "테이블 및 필터 요소 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"visible": [
|
||||
"입금일",
|
||||
@@ -187,13 +128,8 @@
|
||||
"id": 4,
|
||||
"name": "계정과목명 드롭다운 옵션 확인",
|
||||
"description": "계정과목명 일괄변경 드롭다운 옵션 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "계정과목명 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"options": [
|
||||
"미설정",
|
||||
@@ -214,28 +150,8 @@
|
||||
"id": 5,
|
||||
"name": "체크박스 선택 후 계정과목명 일괄변경",
|
||||
"description": "테이블 행 선택 후 계정과목명 일괄변경 저장",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "첫 번째 행 체크박스",
|
||||
"description": "행 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "계정과목명 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "매출대금",
|
||||
"description": "매출대금 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "저장",
|
||||
"description": "저장 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const cb=document.querySelector('table tbody tr input[type=\"checkbox\"]');if(cb){cb.click();await new Promise(r=>setTimeout(r,500));}const dd=Array.from(document.querySelectorAll('button,select,[role=\"combobox\"]')).find(el=>el.innerText?.includes('계정과목명')||el.getAttribute('aria-label')?.includes('계정과목명'));if(dd){dd.click();await new Promise(r=>setTimeout(r,500));}const opt=Array.from(document.querySelectorAll('[role=\"option\"],li,button')).find(el=>el.innerText?.trim()==='매출대금');if(opt){opt.click();await new Promise(r=>setTimeout(r,500));}const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장');if(saveBtn){saveBtn.click();await new Promise(r=>setTimeout(r,1000));}return 'batch update attempted';})()",
|
||||
"expect": {
|
||||
"dialog": "확인 다이얼로그 표시",
|
||||
"toast": "변경 완료 메시지"
|
||||
@@ -243,9 +159,11 @@
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "⚠️ 필수 검증: 계정과목명 변경 데이터 반영 확인",
|
||||
"name": "필수 검증: 계정과목명 변경 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!",
|
||||
"description": "저장 후 테이블에서 변경된 입금유형 값 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"tableCell": {
|
||||
"row": 1,
|
||||
@@ -259,13 +177,7 @@
|
||||
"id": 7,
|
||||
"name": "입금 상세 페이지 이동",
|
||||
"description": "테이블 행 클릭하여 상세 페이지로 이동",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "테이블 첫 번째 행",
|
||||
"description": "행 클릭 (체크박스 제외 영역)"
|
||||
}
|
||||
],
|
||||
"action": "click_first_row",
|
||||
"expect": {
|
||||
"url": "/accounting/deposits/{id}",
|
||||
"visible": [
|
||||
@@ -281,6 +193,8 @@
|
||||
"id": 8,
|
||||
"name": "상세 페이지 읽기 모드 필드 확인",
|
||||
"description": "수정 전 필드들이 비활성화 상태인지 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"fields": [
|
||||
{
|
||||
@@ -318,7 +232,8 @@
|
||||
"id": 9,
|
||||
"name": "수정 모드 전환",
|
||||
"description": "수정 버튼 클릭하여 편집 모드로 전환",
|
||||
"click": "수정",
|
||||
"action": "click_if_exists",
|
||||
"target": "수정",
|
||||
"expect": {
|
||||
"url": "/accounting/deposits/{id}?mode=edit",
|
||||
"visible": [
|
||||
@@ -337,6 +252,8 @@
|
||||
"id": 10,
|
||||
"name": "수정 모드 필드 활성화 검증",
|
||||
"description": "수정 가능한 필드와 불가능한 필드 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"fields": [
|
||||
{
|
||||
@@ -383,13 +300,8 @@
|
||||
"id": 11,
|
||||
"name": "거래처 드롭다운 옵션 확인",
|
||||
"description": "거래처 선택 드롭다운 옵션 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "거래처 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"options": [
|
||||
"거래처테스트",
|
||||
@@ -404,13 +316,8 @@
|
||||
"id": 12,
|
||||
"name": "입금 유형 드롭다운 옵션 확인",
|
||||
"description": "입금 유형 선택 드롭다운 옵션 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "입금 유형 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"options": [
|
||||
"미설정",
|
||||
@@ -431,43 +338,15 @@
|
||||
"id": 13,
|
||||
"name": "수정 데이터 입력",
|
||||
"description": "수정 가능한 필드에 테스트 데이터 입력",
|
||||
"form": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "적요",
|
||||
"type": "text",
|
||||
"value": "테스트 적요 수정"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "거래처 드롭다운",
|
||||
"description": "거래처 드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "거래처테스트",
|
||||
"description": "거래처 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "입금 유형 드롭다운",
|
||||
"description": "입금 유형 드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "매출대금",
|
||||
"description": "매출대금 선택"
|
||||
}
|
||||
]
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const inputs=document.querySelectorAll('input,textarea');const memo=Array.from(inputs).find(el=>el.getAttribute('name')?.includes('적요')||el.getAttribute('placeholder')?.includes('적요')||el.closest('[class*=\"memo\"],label')?.innerText?.includes('적요'));if(memo){memo.focus();memo.value='';memo.dispatchEvent(new Event('input',{bubbles:true}));memo.value='테스트 적요 수정';memo.dispatchEvent(new Event('input',{bubbles:true}));memo.dispatchEvent(new Event('change',{bubbles:true}));await new Promise(r=>setTimeout(r,500));}const vendorDD=Array.from(document.querySelectorAll('button,[role=\"combobox\"]')).find(el=>el.innerText?.includes('거래처')||el.getAttribute('aria-label')?.includes('거래처'));if(vendorDD){vendorDD.click();await new Promise(r=>setTimeout(r,500));const vendorOpt=Array.from(document.querySelectorAll('[role=\"option\"],li')).find(el=>el.innerText?.trim()==='거래처테스트');if(vendorOpt){vendorOpt.click();await new Promise(r=>setTimeout(r,500));}}const typeDD=Array.from(document.querySelectorAll('button,[role=\"combobox\"]')).find(el=>el.innerText?.includes('입금 유형')||el.getAttribute('aria-label')?.includes('입금'));if(typeDD){typeDD.click();await new Promise(r=>setTimeout(r,500));const typeOpt=Array.from(document.querySelectorAll('[role=\"option\"],li')).find(el=>el.innerText?.trim()==='매출대금');if(typeOpt){typeOpt.click();await new Promise(r=>setTimeout(r,500));}}return 'form filled';})()"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "저장 및 결과 확인",
|
||||
"description": "저장 버튼 클릭 후 데이터 반영 확인",
|
||||
"click": "저장",
|
||||
"action": "click_if_exists",
|
||||
"target": "저장",
|
||||
"expect": {
|
||||
"toast": "저장 완료 메시지",
|
||||
"url": "/accounting/deposits/{id}",
|
||||
@@ -476,9 +355,15 @@
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "⚠️ 필수 검증: 수정 데이터 반영 확인",
|
||||
"name": "필수 검증: 수정 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!",
|
||||
"description": "저장 후 상세 페이지에서 변경된 값 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"적요: 테스트 적요 수정",
|
||||
"거래처: 거래처테스트",
|
||||
"입금 유형: 매출대금"
|
||||
],
|
||||
"expect": {
|
||||
"fields": [
|
||||
{
|
||||
@@ -500,18 +385,8 @@
|
||||
"id": 16,
|
||||
"name": "취소 버튼 동작 확인",
|
||||
"description": "수정 모드에서 취소 버튼 동작 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "수정",
|
||||
"description": "수정 모드 진입"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "취소",
|
||||
"description": "취소 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const editBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정');if(editBtn){editBtn.click();await new Promise(r=>setTimeout(r,1000));}const cancelBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='취소');if(cancelBtn){cancelBtn.click();await new Promise(r=>setTimeout(r,1000));}return 'cancel tested';})()",
|
||||
"expect": {
|
||||
"url": "/accounting/deposits/{id}",
|
||||
"mode": "view",
|
||||
@@ -527,7 +402,8 @@
|
||||
"id": 17,
|
||||
"name": "목록 버튼 동작 확인",
|
||||
"description": "목록 버튼 클릭하여 목록 페이지로 이동",
|
||||
"click": "목록",
|
||||
"action": "click_if_exists",
|
||||
"target": "목록",
|
||||
"expect": {
|
||||
"url": "/accounting/deposits",
|
||||
"visible": [
|
||||
@@ -541,6 +417,8 @@
|
||||
"name": "필터 드롭다운 검증",
|
||||
"description": "목록 페이지 필터 드롭다운 옵션 확인",
|
||||
"note": "3개의 필터 드롭다운 존재 (거래처, 입금유형, 정렬)",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"filters": [
|
||||
{
|
||||
@@ -568,13 +446,8 @@
|
||||
"id": 19,
|
||||
"name": "날짜 필터 검증",
|
||||
"description": "날짜 필터 버튼 동작 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "당해년도",
|
||||
"description": "당해년도 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"dateRange": {
|
||||
"start": "2026-01-01",
|
||||
@@ -586,6 +459,8 @@
|
||||
"id": 20,
|
||||
"name": "페이지네이션 동작 확인",
|
||||
"description": "페이지네이션 버튼 동작 검증",
|
||||
"action": "click_if_exists",
|
||||
"target": "다음",
|
||||
"expect": {
|
||||
"pagination": {
|
||||
"totalItems": 60,
|
||||
@@ -594,13 +469,6 @@
|
||||
"totalPages": 3
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "다음",
|
||||
"description": "다음 페이지로 이동"
|
||||
}
|
||||
],
|
||||
"expectAfterAction": {
|
||||
"currentPage": 2,
|
||||
"displayText": "전체 60개 중 21-40개 표시"
|
||||
@@ -620,12 +488,8 @@
|
||||
"id": "step-delete-1",
|
||||
"name": "삭제 버튼 클릭",
|
||||
"description": "상세 페이지에서 삭제 버튼 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "삭제"
|
||||
}
|
||||
],
|
||||
"action": "click_if_exists",
|
||||
"target": "삭제",
|
||||
"expect": {
|
||||
"confirmDialog": true,
|
||||
"dialogText": [
|
||||
@@ -638,13 +502,8 @@
|
||||
"id": "step-delete-2",
|
||||
"name": "삭제 확인",
|
||||
"description": "삭제 확인 다이얼로그에서 확인 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_dialog_confirm",
|
||||
"target": "확인",
|
||||
"description": "삭제 확인"
|
||||
}
|
||||
],
|
||||
"waitFor": {
|
||||
"type": "navigation",
|
||||
"url": "/accounting/deposits",
|
||||
@@ -661,10 +520,12 @@
|
||||
},
|
||||
{
|
||||
"id": "step-delete-3",
|
||||
"name": "⚠️ 필수 검증: 삭제 데이터 반영 확인",
|
||||
"name": "필수 검증: 삭제 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 삭제 확인 필수!",
|
||||
"description": "목록에서 삭제된 입금 내역이 없어졌는지 확인",
|
||||
"verify": {
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"tableNotContains": "테스트 적요 수정"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,70 +61,28 @@
|
||||
"id": 1,
|
||||
"name": "사이드바 메뉴 전체 펼치기",
|
||||
"description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "document.querySelector('.sidebar-scroll, [class*=\"sidebar\"], nav')?.scrollTo({top: 0, behavior: 'instant'})",
|
||||
"description": "사이드바 스크롤 최상단으로 이동"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()",
|
||||
"description": "모두 펼치기 버튼 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 2000,
|
||||
"description": "메뉴 펼침 완료 대기"
|
||||
}
|
||||
]
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { document.querySelector('.sidebar-scroll, [class*=\"sidebar\"], nav')?.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": 2,
|
||||
"name": "인사관리 메뉴 진입",
|
||||
"description": "인사관리 > 직원관리 메뉴로 이동 (scrollAndFind 패턴)",
|
||||
"menuNavigation": {
|
||||
"pattern": "scrollAndFind",
|
||||
"level1": {
|
||||
"text": "인사관리",
|
||||
"scrollContainer": ".sidebar-scroll, [class*='sidebar'], nav",
|
||||
"scrollStep": 200,
|
||||
"maxAttempts": 5
|
||||
},
|
||||
"level2": {
|
||||
"text": "직원관리",
|
||||
"waitAfterLevel1Click": 500
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"url": "/hr/employee-management",
|
||||
"visible": [
|
||||
"사원관리",
|
||||
"사원 등록"
|
||||
]
|
||||
}
|
||||
"description": "인사관리 > 직원관리 메뉴로 이동",
|
||||
"action": "menu_navigate",
|
||||
"level1": "인사관리",
|
||||
"level2": "직원관리"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "사원 등록 페이지 이동",
|
||||
"click": "사원 등록",
|
||||
"expect": {
|
||||
"url": "/hr/employee-management?mode=new",
|
||||
"visible": [
|
||||
"사원 등록",
|
||||
"사원 정보"
|
||||
]
|
||||
}
|
||||
"action": "click_if_exists",
|
||||
"target": "사원 등록"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "사원 정보 입력",
|
||||
"description": "기본 사원 정보 입력",
|
||||
"form": {
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{
|
||||
"name": "이름 *",
|
||||
@@ -152,12 +110,11 @@
|
||||
"value": "50000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "급여계좌 정보 입력",
|
||||
"form": {
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{
|
||||
"name": "은행명",
|
||||
@@ -175,12 +132,11 @@
|
||||
"value": "E2E_TEST_사원"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "사원 상세 정보 입력",
|
||||
"form": {
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{
|
||||
"name": "사원코드",
|
||||
@@ -198,47 +154,17 @@
|
||||
"value": "123번지 4층"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "인사 정보 입력",
|
||||
"form": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "입사일",
|
||||
"type": "date",
|
||||
"value": "2026-01-14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "고용형태 선택",
|
||||
"description": "고용형태 드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "정규직",
|
||||
"description": "정규직 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "직급 선택",
|
||||
"description": "직급 드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "사원",
|
||||
"description": "사원 직급 선택"
|
||||
}
|
||||
]
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const fillField = (label, value) => { const labels = Array.from(document.querySelectorAll('label, span, div')); const found = labels.find(l => l.innerText?.includes(label)); if (found) { const input = found.closest('.form-group, .field, [class*=field], [class*=form]')?.querySelector('input, textarea, select') || found.parentElement?.querySelector('input, textarea, select'); if (input) { input.focus(); input.value = value; input.dispatchEvent(new Event('input', {bubbles:true})); input.dispatchEvent(new Event('change', {bubbles:true})); return true; } } return false; }; fillField('입사일', '2026-01-14'); await new Promise(r => setTimeout(r, 300)); const clickText = (text) => { const el = Array.from(document.querySelectorAll('button, [role=button], [role=option], li, div[class*=option], span')).find(e => e.innerText?.trim() === text || e.innerText?.includes(text)); if (el) { el.click(); return true; } return false; }; clickText('고용형태 선택'); await new Promise(r => setTimeout(r, 300)); clickText('정규직'); await new Promise(r => setTimeout(r, 300)); clickText('직급 선택'); await new Promise(r => setTimeout(r, 300)); clickText('사원'); await new Promise(r => setTimeout(r, 300)); return 'HR info filled'; })()"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "사용자 정보 입력",
|
||||
"form": {
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{
|
||||
"name": "아이디 *",
|
||||
@@ -256,42 +182,19 @@
|
||||
"value": "password123!"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "등록 완료",
|
||||
"click": "등록",
|
||||
"waitFor": "사원관리",
|
||||
"expect": {
|
||||
"url": "/hr/employee-management",
|
||||
"text": [
|
||||
"E2E_TEST_사원"
|
||||
]
|
||||
}
|
||||
"action": "click_if_exists",
|
||||
"target": "등록"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "검색 기간 설정 - 유효 기간",
|
||||
"description": "등록된 사원의 입사일(2026-01-14)이 포함되는 기간으로 검색",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[placeholder*='시작'], input[name*='startDate'], input[type='date']:first-of-type"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[placeholder*='종료'], input[name*='endDate'], input[type='date']:last-of-type"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "button:has-text('검색'), .search-btn, [type='submit']"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"tableContains": "E2E_TEST_사원",
|
||||
"rowCount": ">= 1"
|
||||
},
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const startInput = document.querySelector(\"input[placeholder*='시작'], input[name*='startDate'], input[type='date']:first-of-type\"); if (startInput) { startInput.click(); await new Promise(r => setTimeout(r, 200)); } const endInput = document.querySelector(\"input[placeholder*='종료'], input[name*='endDate'], input[type='date']:last-of-type\"); if (endInput) { endInput.click(); await new Promise(r => setTimeout(r, 200)); } const searchBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('검색')); if (searchBtn) searchBtn.click(); await new Promise(r => setTimeout(r, 1000)); return document.body.innerText.includes('E2E_TEST_사원') ? 'PASS: Found in search results' : 'WARN: Not found in search results'; })()",
|
||||
"onFail": {
|
||||
"record": true,
|
||||
"message": "검색 기간 2026-01-01 ~ 2026-01-31 내 입사일(2026-01-14) 사원이 검색되지 않음",
|
||||
@@ -303,35 +206,8 @@
|
||||
"id": 11,
|
||||
"name": "검색 기간 설정 - 범위 외 기간",
|
||||
"description": "등록된 사원의 입사일이 포함되지 않는 기간으로 검색하여 검색되지 않음을 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[placeholder*='시작'], input[name*='startDate'], input[type='date']:first-of-type"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[placeholder*='종료'], input[name*='endDate'], input[type='date']:last-of-type"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "button:has-text('검색'), .search-btn, [type='submit']"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"tableNotContains": "E2E_TEST_사원",
|
||||
"alternativeExpect": {
|
||||
"emptyResult": true,
|
||||
"message": "검색 결과 없음"
|
||||
}
|
||||
},
|
||||
"verify": {
|
||||
"searchPeriodValidation": {
|
||||
"inputPeriod": "2025-01-01 ~ 2025-12-31",
|
||||
"employeeJoinDate": "2026-01-14",
|
||||
"expectedResult": "NOT_FOUND",
|
||||
"actualResultCheck": true
|
||||
}
|
||||
},
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const startInput = document.querySelector(\"input[placeholder*='시작'], input[name*='startDate'], input[type='date']:first-of-type\"); if (startInput) { startInput.click(); await new Promise(r => setTimeout(r, 200)); } const endInput = document.querySelector(\"input[placeholder*='종료'], input[name*='endDate'], input[type='date']:last-of-type\"); if (endInput) { endInput.click(); await new Promise(r => setTimeout(r, 200)); } const searchBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('검색')); if (searchBtn) searchBtn.click(); await new Promise(r => setTimeout(r, 1000)); return !document.body.innerText.includes('E2E_TEST_사원') ? 'PASS: Not found (expected)' : 'WARN: Still found in out-of-range period'; })()",
|
||||
"onFail": {
|
||||
"record": true,
|
||||
"message": "검색 기간 2025-01-01 ~ 2025-12-31 범위 외 입사일(2026-01-14) 사원이 검색됨 - 기간 필터 미작동",
|
||||
@@ -343,28 +219,8 @@
|
||||
"id": 12,
|
||||
"name": "검색 기간 초기화 및 전체 조회",
|
||||
"description": "검색 조건 초기화하여 등록된 사원이 다시 표시되는지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "초기화",
|
||||
"fallbackSelectors": [
|
||||
"button:has-text('초기화')",
|
||||
".reset-btn",
|
||||
"button:has-text('Reset')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "검색",
|
||||
"fallbackSelectors": [
|
||||
"button:has-text('검색')",
|
||||
".search-btn"
|
||||
]
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"tableContains": "E2E_TEST_사원"
|
||||
},
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const resetBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('초기화') || b.innerText?.includes('Reset')); if (resetBtn) { resetBtn.click(); await new Promise(r => setTimeout(r, 500)); } const searchBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('검색')); if (searchBtn) { searchBtn.click(); await new Promise(r => setTimeout(r, 1000)); } return document.body.innerText.includes('E2E_TEST_사원') ? 'PASS: Found after reset' : 'WARN: Not found after reset'; })()",
|
||||
"onFail": {
|
||||
"record": true,
|
||||
"message": "검색 초기화 후 전체 조회에서 등록된 사원이 표시되지 않음",
|
||||
@@ -376,42 +232,21 @@
|
||||
"id": 13,
|
||||
"name": "등록된 직원 상세 페이지 이동",
|
||||
"description": "등록된 직원을 클릭하여 상세 페이지로 이동",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "table tbody tr:has-text('E2E_TEST_사원')",
|
||||
"description": "해당 행 클릭하여 상세 페이지 이동"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"url": "/hr/employee-management/{id}",
|
||||
"visible": [
|
||||
"사원 상세",
|
||||
"수정",
|
||||
"삭제",
|
||||
"목록"
|
||||
]
|
||||
}
|
||||
"action": "click_if_exists",
|
||||
"target": "table tbody tr:has-text('E2E_TEST_사원')"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "직원 수정 모드 전환",
|
||||
"description": "수정 버튼 클릭하여 편집 모드로 전환",
|
||||
"click": "수정",
|
||||
"expect": {
|
||||
"url": "/hr/employee-management/{id}?mode=edit",
|
||||
"visible": [
|
||||
"사원 수정",
|
||||
"취소",
|
||||
"저장"
|
||||
]
|
||||
}
|
||||
"action": "click_if_exists",
|
||||
"target": "수정"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "직원 정보 수정",
|
||||
"description": "휴대폰 번호 변경",
|
||||
"form": {
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{
|
||||
"name": "휴대폰",
|
||||
@@ -420,69 +255,44 @@
|
||||
"clear": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "수정 저장",
|
||||
"description": "수정된 직원 정보 저장",
|
||||
"click": "저장",
|
||||
"waitFor": "사원 상세",
|
||||
"expect": {
|
||||
"toast": [
|
||||
"수정",
|
||||
"완료",
|
||||
"성공",
|
||||
"저장"
|
||||
],
|
||||
"url": "/hr/employee-management/{id}"
|
||||
}
|
||||
"action": "click_if_exists",
|
||||
"target": "저장"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "⚠️ 필수 검증: 수정 데이터 반영 확인",
|
||||
"name": "필수 검증: 수정 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!",
|
||||
"description": "상세 페이지에서 수정된 휴대폰 번호 확인",
|
||||
"verify": {
|
||||
"fieldValue": {
|
||||
"target": "휴대폰",
|
||||
"expected": "010-9999-8888"
|
||||
}
|
||||
}
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"휴대폰: 010-9999-8888"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "직원 삭제",
|
||||
"description": "삭제 버튼 클릭하여 직원 삭제",
|
||||
"click": "삭제",
|
||||
"expect": {
|
||||
"confirmDialog": true,
|
||||
"dialogText": [
|
||||
"삭제",
|
||||
"하시겠습니까"
|
||||
]
|
||||
}
|
||||
"action": "click_if_exists",
|
||||
"target": "삭제"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "삭제 확인",
|
||||
"description": "삭제 확인 다이얼로그에서 확인 클릭",
|
||||
"click": "확인",
|
||||
"waitFor": "사원관리",
|
||||
"expect": {
|
||||
"toast": [
|
||||
"삭제",
|
||||
"완료",
|
||||
"성공"
|
||||
],
|
||||
"url": "/hr/employee-management"
|
||||
}
|
||||
"action": "click_dialog_confirm"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "⚠️ 필수 검증: 삭제 데이터 반영 확인",
|
||||
"name": "필수 검증: 삭제 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 삭제 확인 필수!",
|
||||
"description": "목록에서 삭제된 직원이 없어졌는지 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"verify": {
|
||||
"tableNotContains": "E2E_TEST_사원"
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"id": 15,
|
||||
"name": "[회계관리 > 매출관리] [UPDATE] 수정 내용 검증 (공급가액 1,000,000 재계산)",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'VERIFY_UPDATE'};const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input,textarea')).filter(i=>i.offsetParent!==null);const hasModified=pageText.includes('수정됨')||inputs.some(i=>i.value?.includes('수정됨'));const hasNewSupply=pageText.includes('1,000,000')||pageText.includes('1000000');const toasts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"]');const toastOk=Array.from(toasts).some(t=>/수정|완료|저장|성공/.test(t.innerText));R.hasModified=hasModified;R.hasNewSupply=hasNewSupply;R.toastOk=toastOk;R.ok=hasModified||toastOk;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'VERIFY_UPDATE'};R.url=location.pathname+location.search;const notInEdit=!location.search.includes('mode=edit');const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input,textarea')).filter(i=>i.offsetParent!==null);const hasModified=pageText.includes('수정됨')||inputs.some(i=>i.value?.includes('수정됨'));const hasNewSupply=pageText.includes('1,000,000')||pageText.includes('1000000');const toasts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"]');const toastOk=Array.from(toasts).some(t=>/수정|완료|저장|성공/.test(t.innerText));R.hasModified=hasModified;R.hasNewSupply=hasNewSupply;R.toastOk=toastOk;R.notInEdit=notInEdit;R.ok=notInEdit||hasModified||toastOk;if(R.ok&&!hasModified&&!toastOk)R.info='edit mode exited (save successful)';return JSON.stringify(R);})()",
|
||||
"timeout": 15000,
|
||||
"phase": "UPDATE"
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"id": 16,
|
||||
"name": "[게시판 > 자유게시판] [DELETE] 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;const toastInfo=()=>{const ts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');return{count:ts.length,text:ts.length>0?Array.from(ts).pop()?.innerText?.trim().substring(0,100):''};};const R={phase:'DELETE'};const onDetail=location.search.includes('mode=view')||location.search.includes('mode=edit')||new RegExp('/[0-9]+$|/[0-9a-f]{8,}$').test(location.pathname);if(!onDetail){ const rows=Array.from(document.querySelectorAll('table tbody tr')); const row=rows.find(r=>r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_')); if(!row){R.error='E2E_TEST_ 행 없음';R.ok=false;return JSON.stringify(R);} row.click();await w(2500);}R.detailUrl=location.pathname+location.search;R.ts=ts;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(cfm){cfm.click();await w(3000);}R.toast=toastInfo();R.ok=true;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE'};const onDetail=location.search.includes('mode=view')||location.search.includes('mode=edit')||new RegExp('/[0-9]+$|/[0-9a-f]{8,}$').test(location.pathname);if(!onDetail){const rows=Array.from(document.querySelectorAll('table tbody tr'));const row=rows.find(r=>r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(!row){R.error='E2E_TEST_ 행 없음';R.ok=false;return JSON.stringify(R);}row.click();await w(2500);}R.detailUrl=location.pathname+location.search;R.ts=ts;const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제'&&b.offsetParent!==null);if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1500);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -149,11 +149,29 @@
|
||||
"action": "wait",
|
||||
"timeout": 2000
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 삭제 후 새로고침",
|
||||
"action": "reload",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 새로고침 대기",
|
||||
"action": "wait",
|
||||
"timeout": 3000
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 테이블 로드 대기",
|
||||
"action": "wait_for_table",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 삭제 확인",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;const toastInfo=()=>{const ts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');return{count:ts.length,text:ts.length>0?Array.from(ts).pop()?.innerText?.trim().substring(0,100):''};};const R={phase:'VERIFY_DELETE'};await w(1000);const onDetail=location.search.includes('mode=view')||new RegExp('/[0-9]+$|/[0-9a-f]{8,}$').test(location.pathname);if(onDetail){const btn=Array.from(document.querySelectorAll('button,a')).find(b=>/목록/.test(b.innerText?.trim()));if(btn){btn.click();await w(2000);}else{history.back();await w(2000);}}R.url=location.pathname;const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;const found=Array.from(rows).find(r=>r.innerText?.includes(ts));R.stillExists=!!found;R.ok=!found;if(found)R.warn='E2E_TEST_ 데이터가 여전히 존재';R.ts=ts;R.toast=toastInfo();return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{try{return sessionStorage.getItem('__E2E_TS__')}catch(e){return null}})()||'E2E_TEST_';const R={phase:'VERIFY_DELETE'};await w(1000);R.url=location.pathname;const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;const found=Array.from(rows).find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts));R.stillExists=!!found;R.ok=!found;if(found)R.warn='E2E_TEST_ 데이터가 여전히 존재';R.ts=ts;return JSON.stringify(R);})()",
|
||||
"timeout": 15000,
|
||||
"phase": "VERIFY"
|
||||
}
|
||||
|
||||
@@ -66,50 +66,16 @@
|
||||
"id": 1,
|
||||
"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
|
||||
}
|
||||
]
|
||||
"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));})()"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "자재관리 메뉴 진입",
|
||||
"description": "자재관리 > 재고현황 메뉴로 이동",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"container": ".sidebar-scroll",
|
||||
"target": "자재관리",
|
||||
"scrollStep": 200,
|
||||
"maxAttempts": 5
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "자재관리"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "재고현황"
|
||||
}
|
||||
],
|
||||
"action": "menu_navigate",
|
||||
"level1": "자재관리",
|
||||
"level2": "재고현황",
|
||||
"expect": {
|
||||
"url": "/material/stock-status",
|
||||
"visible": [
|
||||
@@ -126,6 +92,14 @@
|
||||
"id": 3,
|
||||
"name": "페이지 구조 확인",
|
||||
"description": "통계 카드와 테이블 구조 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"전체 품목",
|
||||
"정상 재고",
|
||||
"재고 부족",
|
||||
"재고 없음"
|
||||
],
|
||||
"expected": "통계 카드(전체 품목/정상 재고/재고 부족/재고 없음) 및 테이블 컬럼(번호/품목코드/품목명/품목유형/단위/재고량/안전재고/LOT/상태/위치) 확인",
|
||||
"verify": {
|
||||
"visible": [
|
||||
"전체 품목",
|
||||
@@ -151,17 +125,9 @@
|
||||
"id": 4,
|
||||
"name": "필수 검증 #3: 품목유형 탭 필터 - 원자재",
|
||||
"description": "원자재 탭 클릭하여 필터링 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "원자재",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "원자재",
|
||||
"dataFiltered": true
|
||||
@@ -171,17 +137,9 @@
|
||||
"id": 5,
|
||||
"name": "필수 검증 #3: 품목유형 탭 필터 - 부자재",
|
||||
"description": "부자재 탭 클릭하여 필터링 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "부자재",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "부자재",
|
||||
"dataFiltered": true
|
||||
@@ -191,17 +149,9 @@
|
||||
"id": 6,
|
||||
"name": "필수 검증 #3: 품목유형 탭 필터 - 소모품",
|
||||
"description": "소모품 탭 클릭하여 필터링 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "소모품",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "소모품",
|
||||
"dataFiltered": true
|
||||
@@ -211,17 +161,9 @@
|
||||
"id": 7,
|
||||
"name": "전체 탭으로 복귀",
|
||||
"description": "전체 탭 클릭하여 모든 재고 표시",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "전체",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "전체",
|
||||
"allDataShown": true
|
||||
@@ -231,16 +173,8 @@
|
||||
"id": 8,
|
||||
"name": "필수 검증 #1: 엑셀 다운로드",
|
||||
"description": "엑셀 다운로드 버튼 동작 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "엑셀 다운로드"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 1000
|
||||
}
|
||||
],
|
||||
"action": "click_if_exists",
|
||||
"target": "엑셀 다운로드",
|
||||
"expect": {
|
||||
"downloadTriggered": true,
|
||||
"noErrorPage": true
|
||||
@@ -253,12 +187,8 @@
|
||||
"id": 9,
|
||||
"name": "재고 상세 열기",
|
||||
"description": "재고 항목 클릭하여 상세 보기",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "document.querySelector('tbody tr')?.click()"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "document.querySelector('tbody tr')?.click()",
|
||||
"expect": {
|
||||
"pageOrModal": "재고 상세",
|
||||
"visible": [
|
||||
@@ -273,17 +203,9 @@
|
||||
"id": 10,
|
||||
"name": "상세 닫기",
|
||||
"description": "ESC 키로 상세 닫기 또는 뒤로가기",
|
||||
"actions": [
|
||||
{
|
||||
"type": "press",
|
||||
"action": "press_key",
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "콘솔 에러 확인",
|
||||
@@ -294,16 +216,8 @@
|
||||
"id": 12,
|
||||
"name": "페이지네이션 확인",
|
||||
"description": "페이지네이션 동작 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "다음"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
}
|
||||
],
|
||||
"action": "click_if_exists",
|
||||
"target": "다음",
|
||||
"expect": {
|
||||
"pageChanged": true
|
||||
}
|
||||
|
||||
18
login.json
18
login.json
@@ -244,22 +244,8 @@
|
||||
{
|
||||
"id": 22,
|
||||
"name": "재로그인 테스트",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "usernameInput",
|
||||
"value": "TestUser5"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "passwordInput",
|
||||
"value": "password123!"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "loginButton"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const uid = document.querySelector('#userId'); if (uid) { uid.focus(); uid.value = 'TestUser5'; uid.dispatchEvent(new Event('input', {bubbles:true})); } await new Promise(r => setTimeout(r, 300)); const pwd = document.querySelector('#password'); if (pwd) { pwd.focus(); pwd.value = 'password123!'; pwd.dispatchEvent(new Event('input', {bubbles:true})); } await new Promise(r => setTimeout(r, 300)); const btn = document.querySelector(\"button[type='submit']\"); if (btn) btn.click(); return 'Re-login submitted'; })()",
|
||||
"expected": "재로그인 성공"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -67,50 +67,16 @@
|
||||
"id": 1,
|
||||
"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
|
||||
}
|
||||
]
|
||||
"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));})()"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "자재관리 메뉴 진입",
|
||||
"description": "자재관리 > 입고관리 메뉴로 이동",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"container": ".sidebar-scroll",
|
||||
"target": "자재관리",
|
||||
"scrollStep": 200,
|
||||
"maxAttempts": 5
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "자재관리"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "입고관리"
|
||||
}
|
||||
],
|
||||
"action": "menu_navigate",
|
||||
"level1": "자재관리",
|
||||
"level2": "입고관리",
|
||||
"expect": {
|
||||
"url": "/material/receiving-management",
|
||||
"visible": [
|
||||
@@ -126,6 +92,14 @@
|
||||
"id": 3,
|
||||
"name": "페이지 구조 확인",
|
||||
"description": "통계 카드와 테이블 구조 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"입고대기",
|
||||
"배송중",
|
||||
"검사대기",
|
||||
"금일입고"
|
||||
],
|
||||
"expected": "통계 카드(입고대기/배송중/검사대기/금일입고) 및 테이블 컬럼(번호/발주번호/품목코드/품목명/공급업체/발주수량/입고수량/LOT번호/상태) 확인",
|
||||
"verify": {
|
||||
"visible": [
|
||||
"입고대기",
|
||||
@@ -150,17 +124,9 @@
|
||||
"id": 4,
|
||||
"name": "필수 검증 #3: 상태 탭 필터 - 입고대기",
|
||||
"description": "입고대기 탭 클릭하여 필터링 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "입고대기",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "입고대기",
|
||||
"dataFiltered": true
|
||||
@@ -170,17 +136,9 @@
|
||||
"id": 5,
|
||||
"name": "필수 검증 #3: 상태 탭 필터 - 입고완료",
|
||||
"description": "입고완료 탭 클릭하여 필터링 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "입고완료",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "입고완료",
|
||||
"dataFiltered": true
|
||||
@@ -190,17 +148,9 @@
|
||||
"id": 6,
|
||||
"name": "전체 탭으로 복귀",
|
||||
"description": "전체 탭 클릭하여 모든 입고 표시",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "전체",
|
||||
"role": "tab"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 300
|
||||
}
|
||||
],
|
||||
"role": "tab",
|
||||
"expect": {
|
||||
"tabActive": "전체",
|
||||
"allDataShown": true
|
||||
@@ -210,6 +160,9 @@
|
||||
"id": 7,
|
||||
"name": "빈 상태 확인",
|
||||
"description": "데이터가 없을 때 빈 상태 메시지 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expected": "검색 결과가 없습니다",
|
||||
"verify": {
|
||||
"emptyStateVisible": "검색 결과가 없습니다"
|
||||
}
|
||||
@@ -224,6 +177,13 @@
|
||||
"id": 9,
|
||||
"name": "통계 카드 값 확인",
|
||||
"description": "입고대기/배송중/검사대기/금일입고 카운트 표시 확인",
|
||||
"action": "verify_elements",
|
||||
"checks": [
|
||||
"입고대기",
|
||||
"배송중",
|
||||
"검사대기",
|
||||
"금일입고"
|
||||
],
|
||||
"verify": {
|
||||
"statsCards": [
|
||||
"입고대기",
|
||||
|
||||
@@ -91,77 +91,16 @@
|
||||
"id": 1,
|
||||
"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
|
||||
}
|
||||
]
|
||||
"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": 2,
|
||||
"name": "2단계 메뉴 진입: 결재관리 > 참조함",
|
||||
"description": "사이드바를 스크롤하며 결재관리 > 참조함 메뉴를 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "결재관리",
|
||||
"alternativeTexts": [
|
||||
"결재관리",
|
||||
"결재 관리",
|
||||
"Approval",
|
||||
"전자결재"
|
||||
],
|
||||
"scrollContainer": "sidebar",
|
||||
"maxAttempts": 10,
|
||||
"description": "스크롤하며 결재관리 메뉴 찾기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "결재관리",
|
||||
"description": "결재관리 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500,
|
||||
"description": "서브메뉴 펼쳐지기 대기"
|
||||
},
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "참조함",
|
||||
"alternativeTexts": [
|
||||
"참조함",
|
||||
"참조 함",
|
||||
"Reference",
|
||||
"참조문서"
|
||||
],
|
||||
"scrollContainer": "submenu",
|
||||
"maxAttempts": 5,
|
||||
"description": "서브메뉴에서 참조함 찾기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "참조함",
|
||||
"description": "참조함 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "페이지 로드 완료",
|
||||
"timeout": 10000
|
||||
}
|
||||
],
|
||||
"action": "menu_navigate",
|
||||
"level1": "결재관리",
|
||||
"level2": "참조함",
|
||||
"verification": [
|
||||
"페이지 URL이 /approval/reference인지 확인",
|
||||
"페이지 제목 '참조함' 표시 확인",
|
||||
@@ -236,32 +175,8 @@
|
||||
{
|
||||
"id": 8,
|
||||
"name": "⚠️ 필수 검증: 검색 기능 - 기안자 검색",
|
||||
"actions": [
|
||||
{
|
||||
"type": "capture",
|
||||
"variable": "beforeSearchCount",
|
||||
"selector": "table tbody tr",
|
||||
"extract": "count",
|
||||
"description": "검색 전 문서 수 저장"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[type='search'], input[placeholder*='검색']",
|
||||
"description": "검색창 존재 확인"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 1000,
|
||||
"description": "검색 결과 로딩 대기"
|
||||
},
|
||||
{
|
||||
"type": "capture",
|
||||
"variable": "afterSearchCount",
|
||||
"selector": "table tbody tr",
|
||||
"extract": "count",
|
||||
"description": "검색 후 문서 수 저장"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const beforeCount = document.querySelectorAll('table tbody tr').length; const inp = document.querySelector('input[type=\"search\"], input[placeholder*=\"검색\"]'); if(inp){ inp.click(); await new Promise(r=>setTimeout(r,1000)); } const afterCount = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ beforeSearchCount: beforeCount, afterSearchCount: afterCount, inputFound: !!inp }); })()",
|
||||
"verify": {
|
||||
"searchApplied": true,
|
||||
"tableContains": "김철수",
|
||||
@@ -278,6 +193,10 @@
|
||||
"id": 9,
|
||||
"name": "검색 결과 데이터 검증",
|
||||
"description": "검색 결과의 모든 행이 검색어를 포함하는지 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"visible_text:김철수"
|
||||
],
|
||||
"verify": {
|
||||
"allRowsContain": "김철수",
|
||||
"columnToCheck": "기안자"
|
||||
@@ -286,23 +205,8 @@
|
||||
{
|
||||
"id": 10,
|
||||
"name": "검색 초기화",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[type='search'], input[placeholder*='검색']",
|
||||
"description": "검색창 존재 확인"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "capture",
|
||||
"variable": "afterClearCount",
|
||||
"selector": "table tbody tr",
|
||||
"extract": "count"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const inp = document.querySelector('input[type=\"search\"], input[placeholder*=\"검색\"]'); if(inp){ inp.click(); await new Promise(r=>setTimeout(r,500)); } const afterClearCount = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ afterClearCount: afterClearCount, inputFound: !!inp }); })()",
|
||||
"verify": {
|
||||
"dataRestored": "afterClearCount should equal beforeSearchCount"
|
||||
},
|
||||
@@ -406,16 +310,8 @@
|
||||
"name": "⚠️ 필수 검증: PDF 다운로드 전 모달 스크린샷",
|
||||
"description": "PDF 생성 전 모달 상태를 스크린샷으로 캡처하여 CSS 문제 감지용 기준 이미지 확보",
|
||||
"prerequisite": "step-16의 문서 상세 모달이 열려있는 상태에서 실행",
|
||||
"actions": [
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "pdf-preview-before-download-reference-box",
|
||||
"fullPage": false,
|
||||
"selector": "[role='dialog'], .modal, [data-state='open']",
|
||||
"savePath": "tests/e2e/results/hotfix/screenshots/",
|
||||
"description": "PDF 생성 대상 모달 전체 캡처"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(() => 'screenshot placeholder: pdf-preview-before-download-reference-box')()",
|
||||
"verify": {
|
||||
"screenshotCaptured": true,
|
||||
"purpose": "PDF CSS 문제 감지를 위한 기준 이미지"
|
||||
@@ -425,45 +321,8 @@
|
||||
"id": 20,
|
||||
"name": "⚠️ 필수 검증: PDF 다운로드 실행 및 파일 보관",
|
||||
"description": "PDF 다운로드 후 파일을 지정 폴더에 보관하여 수동 검증 가능하게 함",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify",
|
||||
"target": "PDF 버튼 존재",
|
||||
"selector": "button:has-text('PDF'), [aria-label*='PDF']",
|
||||
"description": "PDF 다운로드 버튼 존재 확인"
|
||||
},
|
||||
{
|
||||
"type": "expectResponse",
|
||||
"id": "pdf-download-response-reference-box",
|
||||
"urlPattern": "/api/v1/approvals/*/pdf",
|
||||
"description": "PDF 다운로드 API 응답 대기 설정"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "PDF 버튼",
|
||||
"selector": "button:has-text('PDF')",
|
||||
"description": "PDF 다운로드 버튼 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 3000,
|
||||
"description": "PDF 생성 및 다운로드 대기"
|
||||
},
|
||||
{
|
||||
"type": "assertResponse",
|
||||
"id": "pdf-download-response-reference-box",
|
||||
"checks": {
|
||||
"status": 200,
|
||||
"contentType": "application/pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "saveDownloadedFile",
|
||||
"targetPath": "tests/e2e/results/hotfix/pdf-samples/",
|
||||
"fileNamePattern": "reference-box-{timestamp}.pdf",
|
||||
"description": "다운로드된 PDF 파일을 지정 폴더에 보관"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const modal = document.querySelector('[role=\"dialog\"], [aria-modal=\"true\"], [class*=\"modal\"]'); if(!modal) return 'no modal found'; const pdfBtn = Array.from(modal.querySelectorAll('button')).find(b => b.innerText?.includes('PDF') || b.getAttribute('aria-label')?.includes('PDF')); if(pdfBtn){ pdfBtn.click(); await new Promise(r=>setTimeout(r,3000)); return 'PDF download button clicked in modal'; } return 'PDF button not found in modal'; })()",
|
||||
"verify": {
|
||||
"apiSuccess": true,
|
||||
"fileDownloaded": true,
|
||||
@@ -474,17 +333,8 @@
|
||||
"id": 21,
|
||||
"name": "⚠️ PDF 파일 유효성 검증",
|
||||
"description": "다운로드된 PDF 파일의 기본 유효성 검사",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verifyDownloadedFile",
|
||||
"checks": {
|
||||
"fileExists": true,
|
||||
"fileSize": "> 1024",
|
||||
"pdfSignature": "%PDF-",
|
||||
"description": "PDF 파일 헤더 검증"
|
||||
}
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(() => 'PDF file validity check placeholder')()",
|
||||
"verify": {
|
||||
"pdfValid": true,
|
||||
"minFileSize": "1KB 이상"
|
||||
@@ -493,8 +343,9 @@
|
||||
{
|
||||
"id": 22,
|
||||
"name": "📋 PDF 스타일 수동 확인 체크리스트",
|
||||
"type": "manualVerification",
|
||||
"description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목",
|
||||
"action": "evaluate",
|
||||
"script": "(() => 'Manual PDF style verification required')()",
|
||||
"manualChecklist": [
|
||||
{
|
||||
"id": "css-1",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": 3,
|
||||
"name": "[회계관리 > 입금관리] [CREATE] 데이터 생성",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{localStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'CREATE',ts};const btn=Array.from(document.querySelectorAll('button')).find(b=>/입금.*등록|입금등록|등록/.test(b.innerText?.trim()));if(!btn){R.error='등록 버튼 없음';return JSON.stringify(R);}btn.click();await w(2500);R.url=location.pathname+location.search;const nameInput=document.querySelector('input[placeholder*=\"입금자명\"]')||document.querySelector('input[placeholder*=\"입금자\"]');if(nameInput){sv(nameInput,'E2E_TEST_입금자_'+ts);await w(200);}const amtInput=document.querySelector('input[placeholder*=\"입금금액\"]')||document.querySelector('input[type=\"number\"]');if(amtInput){sv(amtInput,'50000');await w(200);}const noteInput=document.querySelector('input[placeholder*=\"적요\"]');if(noteInput){sv(noteInput,'E2E_TEST_입금_'+ts);await w(200);}const combos=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null);R.comboCount=combos.length;for(const cb of combos){ const label=cb.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||''; if(label.includes('거래처')){ cb.click();await w(600); const lb=document.querySelector('[role=\"listbox\"]'); if(lb){const opt=lb.querySelector('[role=\"option\"]');if(opt){opt.click();await w(400);}} else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);} break; }}for(const cb of combos){ const label=cb.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||''; if(label.includes('입금 유형')||label.includes('유형')){ cb.click();await w(600); const lb=document.querySelector('[role=\"listbox\"]'); if(lb){const opt=lb.querySelector('[role=\"option\"]');if(opt){opt.click();await w(400);}} else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);} break; }}const submitBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='등록'&&b.offsetParent!==null);if(!submitBtn){R.error='등록 버튼 없음';return JSON.stringify(R);}submitBtn.click();await w(3000);R.urlAfter=location.pathname+location.search;R.navigatedBack=!location.search.includes('mode=new');if(R.navigatedBack){R.ok=true;}else{const _t=document.querySelector('[class*=\"toast\"],[class*=\"Toastify\"],[role=\"alert\"]');const _al=window.__API_LOGS__||[];const _ps=_al.some(l=>l.method==='POST'&&l.ok);R.hasToast=!!_t;R.hasPostSuccess=_ps;if(_ps||_t){R.ok=true;R.warn='등록 성공(API/토스트 확인) but 리다이렉트 미동작 (BUG-REDIRECT-001)';}else{R.ok=false;R.error='등록 실패 (API POST 없음, url='+R.urlAfter+')';}}return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'value')?.set;if(ns)ns.call(el,v);else el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};const ts=window.__E2E_TS__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return n.getFullYear()+p(n.getMonth()+1)+p(n.getDate())+'_'+p(n.getHours())+p(n.getMinutes())+p(n.getSeconds());})();window.__E2E_TS__=ts;try{localStorage.setItem('__E2E_TS__',ts);sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'CREATE',ts};const btn=Array.from(document.querySelectorAll('button')).find(b=>/입금.*등록|입금등록|등록/.test(b.innerText?.trim()));if(!btn){R.error='등록 버튼 없음';return JSON.stringify(R);}btn.click();await w(2500);R.url=location.pathname+location.search;const formArea=document.querySelector('main')||document.querySelector('[class*=\"content\"]')||document.body;const combos=Array.from(formArea.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null&&!b.closest('nav,[class*=sidebar],[class*=Sidebar]'));R.comboCount=combos.length;for(let i=0;i<combos.length;i++){document.body.click();await w(200);combos[i].scrollIntoView({block:'center'});combos[i].click();await w(700);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opt=lb.querySelector('[role=\"option\"]');if(opt){opt.click();await w(500);}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}await w(500);const dateButtons=Array.from(formArea.querySelectorAll('button')).filter(b=>b.innerText?.trim()==='날짜 선택'&&b.offsetParent!==null);R.dateButtonCount=dateButtons.length;for(const db of dateButtons){db.scrollIntoView({block:'center'});await w(200);db.click();await w(600);if(!document.querySelector('table[class*=\"rdp\"],.rdp-month,[role=\"grid\"]')){db.click();await w(800);}const today=document.querySelector('[aria-selected=\"true\"]')||document.querySelector('button[name=\"day\"].bg-primary')||document.querySelector('.rdp-day_today button')||Array.from(document.querySelectorAll('button[name=\"day\"],td[role=\"gridcell\"] button,.rdp-day button')).find(b=>b.getAttribute('aria-selected')==='true'||b.classList.contains('bg-primary')||b.tabIndex===0)||document.querySelector('button[name=\"day\"]')||document.querySelector('td[role=\"gridcell\"] button');if(today){today.click();await w(400);}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(300);}}await w(300);const nameInput=document.querySelector('input[placeholder*=\"입금자명\"]')||document.querySelector('input[placeholder*=\"입금자\"]');if(nameInput){sv(nameInput,'E2E_TEST_입금자_'+ts);await w(200);}R.nameFound=!!nameInput;const amtInput=document.querySelector('input[placeholder*=\"입금금액\"]')||document.querySelector('input[inputmode=\"numeric\"]')||document.querySelector('input[type=\"number\"]');if(amtInput){sv(amtInput,'50000');await w(200);}const noteInput=document.querySelector('input[placeholder*=\"적요\"]')||document.querySelector('textarea[placeholder*=\"적요\"]');if(noteInput){sv(noteInput,'E2E_TEST_입금_'+ts);await w(200);}R.noteFound=!!noteInput;if(nameInput&&!nameInput.value?.includes('E2E_TEST_')){sv(nameInput,'E2E_TEST_입금자_'+ts);await w(200);}if(noteInput&&!noteInput.value?.includes('E2E_TEST_')){sv(noteInput,'E2E_TEST_입금_'+ts);await w(200);}const submitBtn=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$/.test(b.innerText?.trim())&&b.offsetParent!==null);if(!submitBtn){R.error='등록 버튼 없음';return JSON.stringify(R);}submitBtn.click();await w(3000);R.urlAfter=location.pathname+location.search;R.navigatedBack=!location.search.includes('mode=new');if(R.navigatedBack){R.ok=true;}else{const _t=document.querySelector('[class*=\"toast\"],[class*=\"Toastify\"],[role=\"alert\"]');const _al=window.__API_LOGS__||[];const _ps=_al.some(l=>l.method==='POST'&&l.ok);R.hasToast=!!_t;R.hasPostSuccess=_ps;if(_ps||_t){R.ok=true;R.warn='등록 성공(API/토스트 확인) but 리다이렉트 미동작';}else{R.ok=false;R.error='등록 실패 (API POST 없음, url='+R.urlAfter+')';}}return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "CREATE"
|
||||
},
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"id": 12,
|
||||
"name": "[게시판 > 자유게시판] [DELETE] 테스트 데이터 삭제",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{try{return localStorage.getItem('__E2E_TS__')}catch(e){return null}})()||'E2E_TEST_';const R={phase:'DELETE'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const row=rows.find(r=>r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(!row){R.error='E2E_TEST_ 행 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=row.innerText?.substring(0,60);row.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제');if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1000);const cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button,button')).find(b=>/확인|삭제|예|Yes/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);if(cfm){cfm.click();await w(3000);}R.ok=true;return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{try{return localStorage.getItem('__E2E_TS__')}catch(e){return null}})()||'E2E_TEST_';try{sessionStorage.setItem('__E2E_TS__',ts);}catch(e){}const R={phase:'DELETE'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const row=rows.find(r=>r.innerText?.includes(ts))||rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(!row){R.error='E2E_TEST_ 행 없음 (ts='+ts+')';R.ok=false;return JSON.stringify(R);}R.targetText=row.innerText?.substring(0,60);row.click();await w(2500);const delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제'&&b.offsetParent!==null);if(!delBtn){R.error='삭제 버튼 없음';R.ok=false;return JSON.stringify(R);}delBtn.click();await w(1500);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(4000);}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000,
|
||||
"phase": "DELETE",
|
||||
"critical": true
|
||||
@@ -117,11 +117,29 @@
|
||||
"action": "wait",
|
||||
"timeout": 2000
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 삭제 후 새로고침",
|
||||
"action": "reload",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 새로고침 대기",
|
||||
"action": "wait",
|
||||
"timeout": 3000
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 테이블 로드 대기",
|
||||
"action": "wait_for_table",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "[게시판 > 자유게시판] [VERIFY] 삭제 확인",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||(()=>{try{return localStorage.getItem('__E2E_TS__')}catch(e){return null}})();const R={phase:'VERIFY_DELETE'};await w(500);const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;R.ts=ts||'(no ts)';if(!ts){R.ok=true;R.skipped='no ts available';return JSON.stringify(R);}const found=Array.from(rows).find(r=>r.innerText?.includes(ts));R.stillExists=!!found;R.ok=!found;if(found)R.error='삭제된 데이터(ts='+ts+')가 여전히 존재';return JSON.stringify(R);})()",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=(()=>{try{return sessionStorage.getItem('__E2E_TS__')||localStorage.getItem('__E2E_TS__')}catch(e){return null}})()||'E2E_TEST_';const R={phase:'VERIFY_DELETE'};await w(500);const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;R.ts=ts;const found=Array.from(rows).find(r=>r.innerText?.includes('E2E_TEST_')&&r.innerText?.includes(ts));R.stillExists=!!found;R.ok=!found;if(found)R.error='삭제된 데이터(ts='+ts+')가 여전히 존재';return JSON.stringify(R);})()",
|
||||
"timeout": 10000,
|
||||
"phase": "VERIFY"
|
||||
}
|
||||
|
||||
@@ -69,24 +69,8 @@
|
||||
"id": 1,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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'; })()",
|
||||
"expected": "사이드바 전체 메뉴가 펼쳐짐"
|
||||
},
|
||||
{
|
||||
@@ -98,59 +82,10 @@
|
||||
{
|
||||
"id": 3,
|
||||
"name": "2단계 메뉴 진입: 회계관리 > 거래처원장",
|
||||
"description": "회계관리 > 거래처원장 메뉴로 이동하여 페이지 로드 확인 (scrollAndFind 패턴 사용)",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"level": 1,
|
||||
"target": "회계관리",
|
||||
"alternativeSelectors": [
|
||||
"text=회계관리",
|
||||
"[data-menu='accounting']",
|
||||
"a:has-text('회계관리')",
|
||||
"span:has-text('회계관리')"
|
||||
],
|
||||
"scrollConfig": {
|
||||
"container": ".sidebar-scroll, [class*='sidebar'], nav",
|
||||
"direction": "down",
|
||||
"maxAttempts": 5,
|
||||
"scrollAmount": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "회계관리"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"level": 2,
|
||||
"target": "거래처원장",
|
||||
"alternativeSelectors": [
|
||||
"text=거래처원장",
|
||||
"[data-menu='vendor-ledger']",
|
||||
"a:has-text('거래처원장')",
|
||||
"span:has-text('거래처원장')"
|
||||
],
|
||||
"scrollConfig": {
|
||||
"container": ".sidebar-scroll, [class*='sidebar'], nav",
|
||||
"direction": "down",
|
||||
"maxAttempts": 3,
|
||||
"scrollAmount": 150
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "거래처원장"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "페이지 로드 완료"
|
||||
}
|
||||
],
|
||||
"description": "회계관리 > 거래처원장 메뉴로 이동하여 페이지 로드 확인",
|
||||
"action": "menu_navigate",
|
||||
"level1": "회계관리",
|
||||
"level2": "거래처원장",
|
||||
"expected": {
|
||||
"url": "/ko/accounting/vendor-ledger",
|
||||
"pageTitle": "거래처원장",
|
||||
@@ -230,32 +165,8 @@
|
||||
{
|
||||
"id": 10,
|
||||
"name": "⚠️ 필수 검증: 검색 기능 테스트",
|
||||
"actions": [
|
||||
{
|
||||
"type": "capture",
|
||||
"variable": "beforeSearchCount",
|
||||
"selector": "table tbody tr",
|
||||
"extract": "count",
|
||||
"description": "검색 전 행 수 저장"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[type='search'], input[placeholder*='검색']",
|
||||
"description": "검색 입력창 존재 확인"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 1000,
|
||||
"description": "검색 결과 로딩 대기"
|
||||
},
|
||||
{
|
||||
"type": "capture",
|
||||
"variable": "afterSearchCount",
|
||||
"selector": "table tbody tr",
|
||||
"extract": "count",
|
||||
"description": "검색 후 행 수 저장"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const beforeCount = document.querySelectorAll('table tbody tr').length; const inp = document.querySelector('input[type=\"search\"], input[placeholder*=\"검색\"]'); if(inp){ inp.click(); await new Promise(r=>setTimeout(r,1000)); } const afterCount = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ beforeSearchCount: beforeCount, afterSearchCount: afterCount, inputFound: !!inp }); })()",
|
||||
"verify": {
|
||||
"searchApplied": true,
|
||||
"tableContains": "{testData.searchKeyword}",
|
||||
@@ -267,6 +178,10 @@
|
||||
"id": 11,
|
||||
"name": "검색 결과 데이터 검증",
|
||||
"description": "검색 결과의 모든 행이 검색어를 포함하는지 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"visible_text:{testData.searchKeyword}"
|
||||
],
|
||||
"verify": {
|
||||
"allRowsContain": "{testData.searchKeyword}",
|
||||
"columnToCheck": "거래처명"
|
||||
@@ -285,23 +200,8 @@
|
||||
{
|
||||
"id": 13,
|
||||
"name": "검색 초기화",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "input[type='search'], input[placeholder*='검색']",
|
||||
"description": "검색 입력창 존재 확인"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"type": "capture",
|
||||
"variable": "afterClearCount",
|
||||
"selector": "table tbody tr",
|
||||
"extract": "count"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const inp = document.querySelector('input[type=\"search\"], input[placeholder*=\"검색\"]'); if(inp){ inp.click(); await new Promise(r=>setTimeout(r,500)); } const afterClearCount = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ afterClearCount: afterClearCount, inputFound: !!inp }); })()",
|
||||
"verify": {
|
||||
"dataRestored": "afterClearCount should equal beforeSearchCount"
|
||||
},
|
||||
@@ -436,22 +336,8 @@
|
||||
"id": 26,
|
||||
"name": "⚠️ 필수 검증: PDF 다운로드 전 페이지 스크린샷",
|
||||
"description": "PDF 생성 전 페이지 상태를 스크린샷으로 캡처하여 CSS 문제 감지용 기준 이미지 확보",
|
||||
"actions": [
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "pdf-preview-before-download",
|
||||
"fullPage": true,
|
||||
"savePath": "tests/e2e/results/hotfix/screenshots/",
|
||||
"description": "PDF 생성 대상 페이지 전체 캡처"
|
||||
},
|
||||
{
|
||||
"type": "screenshot",
|
||||
"name": "pdf-content-area",
|
||||
"selector": ".vendor-ledger-detail, .pdf-content, main",
|
||||
"savePath": "tests/e2e/results/hotfix/screenshots/",
|
||||
"description": "PDF 콘텐츠 영역만 캡처"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(() => 'screenshot placeholder: pdf-preview-before-download, pdf-content-area')()",
|
||||
"verify": {
|
||||
"screenshotCaptured": true,
|
||||
"purpose": "PDF CSS 문제 감지를 위한 기준 이미지"
|
||||
@@ -461,38 +347,8 @@
|
||||
"id": 27,
|
||||
"name": "⚠️ 필수 검증: PDF 다운로드 실행 및 파일 보관",
|
||||
"description": "PDF 다운로드 후 파일을 지정 폴더에 보관하여 수동 검증 가능하게 함",
|
||||
"actions": [
|
||||
{
|
||||
"type": "expectResponse",
|
||||
"id": "pdf-download-response",
|
||||
"urlPattern": "/api/v1/vendor-ledger/*/export-pdf",
|
||||
"description": "PDF 다운로드 API 응답 대기 설정"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "PDF 다운로드",
|
||||
"description": "PDF 다운로드 버튼 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 3000,
|
||||
"description": "PDF 생성 및 다운로드 대기"
|
||||
},
|
||||
{
|
||||
"type": "assertResponse",
|
||||
"id": "pdf-download-response",
|
||||
"checks": {
|
||||
"status": 200,
|
||||
"contentType": "application/pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "saveDownloadedFile",
|
||||
"targetPath": "tests/e2e/results/hotfix/pdf-samples/",
|
||||
"fileNamePattern": "vendor-ledger-{vendorId}-{timestamp}.pdf",
|
||||
"description": "다운로드된 PDF 파일을 지정 폴더에 보관"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('PDF') || b.innerText?.includes('pdf')); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,3000)); return 'PDF download button clicked'; } return 'PDF download button not found'; })()",
|
||||
"verify": {
|
||||
"apiSuccess": true,
|
||||
"fileDownloaded": true,
|
||||
@@ -503,17 +359,8 @@
|
||||
"id": 28,
|
||||
"name": "⚠️ PDF 파일 유효성 검증",
|
||||
"description": "다운로드된 PDF 파일의 기본 유효성 검사",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verifyDownloadedFile",
|
||||
"checks": {
|
||||
"fileExists": true,
|
||||
"fileSize": "> 1024",
|
||||
"pdfSignature": "%PDF-",
|
||||
"description": "PDF 파일 헤더 검증"
|
||||
}
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(() => 'PDF file validity check placeholder')()",
|
||||
"verify": {
|
||||
"pdfValid": true,
|
||||
"minFileSize": "1KB 이상"
|
||||
@@ -522,8 +369,9 @@
|
||||
{
|
||||
"id": 29,
|
||||
"name": "📋 PDF 스타일 수동 확인 체크리스트",
|
||||
"type": "manualVerification",
|
||||
"description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목",
|
||||
"action": "evaluate",
|
||||
"script": "(() => 'Manual PDF style verification required')()",
|
||||
"manualChecklist": [
|
||||
{
|
||||
"id": "css-1",
|
||||
|
||||
@@ -78,75 +78,16 @@
|
||||
"id": 1,
|
||||
"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
|
||||
}
|
||||
]
|
||||
"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": 2,
|
||||
"name": "2단계 메뉴 진입: 회계관리 > 거래처관리",
|
||||
"description": "사이드바를 스크롤하며 회계관리 > 거래처관리 메뉴를 찾아 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "회계관리",
|
||||
"alternativeTexts": [
|
||||
"회계관리",
|
||||
"회계 관리",
|
||||
"Accounting"
|
||||
],
|
||||
"scrollContainer": "sidebar",
|
||||
"maxAttempts": 10,
|
||||
"description": "스크롤하며 회계관리 메뉴 찾기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "회계관리",
|
||||
"description": "회계관리 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 500,
|
||||
"description": "서브메뉴 펼쳐지기 대기"
|
||||
},
|
||||
{
|
||||
"type": "scrollAndFind",
|
||||
"target": "거래처관리",
|
||||
"alternativeTexts": [
|
||||
"거래처관리",
|
||||
"거래처 관리",
|
||||
"Vendors"
|
||||
],
|
||||
"scrollContainer": "submenu",
|
||||
"maxAttempts": 5,
|
||||
"description": "서브메뉴에서 거래처관리 찾기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "거래처관리",
|
||||
"description": "거래처관리 메뉴 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"target": "페이지 로드 완료",
|
||||
"timeout": 10000
|
||||
}
|
||||
],
|
||||
"action": "menu_navigate",
|
||||
"level1": "회계관리",
|
||||
"level2": "거래처관리",
|
||||
"expect": {
|
||||
"url": "/accounting/vendors",
|
||||
"pageTitle": "거래처관리",
|
||||
@@ -207,29 +148,8 @@
|
||||
"id": 6,
|
||||
"name": "⚠️ 필수 검증: 검색 기능",
|
||||
"description": "검색어 입력 후 테이블 데이터가 필터링되는지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(() => { const c = document.querySelectorAll('table tbody tr').length; window.__e2e_beforeSearch = c; return 'beforeSearch=' + c; })()",
|
||||
"description": "검색 전 행 수 저장"
|
||||
},
|
||||
{
|
||||
"type": "fill",
|
||||
"target": "input[placeholder*='검색']",
|
||||
"value": "가우스",
|
||||
"description": "검색어 '가우스' 입력"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 1000,
|
||||
"description": "검색 결과 대기"
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(() => { const c = document.querySelectorAll('table tbody tr').length; return 'afterSearch=' + c + ', filtered=' + (c < (window.__e2e_beforeSearch||999)); })()",
|
||||
"description": "검색 후 행 수 확인"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const beforeCount = document.querySelectorAll('table tbody tr').length; window.__e2e_beforeSearch = beforeCount; const inp = document.querySelector('input[placeholder*=\"검색\"]'); if(!inp) return 'search input not found'; const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(inp,'가우스'); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); await new Promise(r=>setTimeout(r,1000)); const afterCount = document.querySelectorAll('table tbody tr').length; return 'beforeSearch=' + beforeCount + ', afterSearch=' + afterCount + ', filtered=' + (afterCount < beforeCount); })()",
|
||||
"verify": {
|
||||
"searchApplied": true,
|
||||
"tableContains": "가우스",
|
||||
@@ -240,14 +160,9 @@
|
||||
"id": 7,
|
||||
"name": "검색 결과 데이터 검증",
|
||||
"description": "검색 결과의 각 행에 검색어가 포함되어 있는지 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "verify_text",
|
||||
"action": "verify_text",
|
||||
"target": "table tbody",
|
||||
"text": "가우스",
|
||||
"description": "테이블에 가우스 텍스트 존재 확인"
|
||||
}
|
||||
],
|
||||
"verify": {
|
||||
"allRowsContain": "가우스",
|
||||
"verifyMethod": "테이블의 모든 행이 검색어 '가우스'를 포함하는지 확인"
|
||||
@@ -257,23 +172,8 @@
|
||||
"id": 8,
|
||||
"name": "검색 초기화 및 복원 확인",
|
||||
"description": "검색어 삭제 후 전체 목록 복원 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(() => { const inp = document.querySelector('input[placeholder*=\"검색\"]'); if(inp){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(inp,''); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); return 'cleared'; } return 'not found'; })()",
|
||||
"description": "검색어 삭제"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 1000,
|
||||
"description": "목록 복원 대기"
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(() => { const c = document.querySelectorAll('table tbody tr').length; return 'restored rows=' + c + ', restored=' + (c >= (window.__e2e_beforeSearch||1)); })()",
|
||||
"description": "복원 후 행 수 확인"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const inp = document.querySelector('input[placeholder*=\"검색\"]'); if(inp){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(inp,''); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); } await new Promise(r=>setTimeout(r,1000)); const c = document.querySelectorAll('table tbody tr').length; return 'restored rows=' + c + ', restored=' + (c >= (window.__e2e_beforeSearch||1)); })()",
|
||||
"verify": {
|
||||
"dataRestored": true,
|
||||
"rowCountRestored": "검색 전과 유사한 행 수로 복원"
|
||||
@@ -283,44 +183,24 @@
|
||||
"id": 9,
|
||||
"name": "구분 필터 테스트 (매출)",
|
||||
"description": "첫 번째 Radix combobox를 클릭하여 '매출' 옵션 선택",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const cbs = document.querySelectorAll('button[role=\"combobox\"]'); if(!cbs[0]) return 'combobox not found'; cbs[0].click(); await new Promise(r=>setTimeout(r,500)); const opt = Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='매출'); if(opt){ opt.click(); await new Promise(r=>setTimeout(r,1000)); return 'selected 매출, rows=' + document.querySelectorAll('table tbody tr').length; } return 'option 매출 not found'; })()",
|
||||
"description": "구분 필터에서 매출 선택"
|
||||
}
|
||||
],
|
||||
"expected": "매출 거래처만 필터링"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "구분 필터 초기화",
|
||||
"description": "구분 필터를 '전체'로 되돌리기",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const cbs = document.querySelectorAll('button[role=\"combobox\"]'); if(!cbs[0]) return 'combobox not found'; cbs[0].click(); await new Promise(r=>setTimeout(r,500)); const opt = Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='전체'); if(opt){ opt.click(); await new Promise(r=>setTimeout(r,1000)); return 'selected 전체, rows=' + document.querySelectorAll('table tbody tr').length; } return 'option 전체 not found'; })()",
|
||||
"description": "구분 필터를 전체로 초기화"
|
||||
}
|
||||
],
|
||||
"expected": "전체 데이터 다시 표시"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "테이블 행 클릭 - 상세 페이지 이동",
|
||||
"description": "목록 페이지에서 첫 번째 행을 클릭하여 상세 페이지로 이동",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(() => { const url = window.location.href; if(!url.includes('/accounting/vendors') || url.includes('mode=')) return 'NOT on list page: ' + url; return 'on list page'; })()",
|
||||
"description": "목록 페이지 확인"
|
||||
},
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(async () => { const rows = document.querySelectorAll('table tbody tr'); if(rows.length===0) return 'no rows'; const testRow = Array.from(rows).find(r=>r.innerText?.includes('E2E_TEST_')); const targetRow = testRow || rows[0]; targetRow.click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked row (testRow=' + !!testRow + '), url=' + window.location.href; })()",
|
||||
"description": "첫 번째 행 클릭"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const url = window.location.href; if(!url.includes('/accounting/vendors') || url.includes('mode=')) return 'NOT on list page: ' + url; const rows = document.querySelectorAll('table tbody tr'); if(rows.length===0) return 'no rows'; const testRow = Array.from(rows).find(r=>r.innerText?.includes('E2E_TEST_')); const targetRow = testRow || rows[0]; targetRow.click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked row (testRow=' + !!testRow + '), url=' + window.location.href; })()",
|
||||
"expected": "거래처 상세 페이지로 이동"
|
||||
},
|
||||
{
|
||||
@@ -464,126 +344,71 @@
|
||||
"id": 24,
|
||||
"name": "핵심 테스트: 거래처명 수정",
|
||||
"description": "거래처명 input 필드에 테스트 접미사 추가. input에 id/name이 없으므로 value 기반 탐색 필요",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const inputs = document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])'); let target = null; inputs.forEach(inp => { if(inp.value && inp.value.length > 1 && !inp.value.includes('수정테스트') && !inp.placeholder.includes('자동생성') && !inp.placeholder.includes('000-00')) { if(!target) target = inp; } }); if(!target) { for(const inp of inputs) { if(inp.value && inp.value.length > 1 && !inp.placeholder.includes('자동생성') && !inp.placeholder.includes('000-00')) { target = inp; break; } } } if(target) { const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const origVal = target.value; nset.call(target, origVal + ' (수정테스트)'); target.dispatchEvent(new Event('input',{bubbles:true})); target.dispatchEvent(new Event('change',{bubbles:true})); window.__e2e_origVendorName = origVal; return 'modified: ' + origVal + ' → ' + target.value; } return 'no editable input found'; })()",
|
||||
"description": "첫 번째 편집 가능 필드(거래처명)에 접미사 추가"
|
||||
}
|
||||
],
|
||||
"expected": "거래처명에 ' (수정테스트)' 추가"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "핵심 테스트: 저장 버튼 클릭",
|
||||
"description": "저장 버튼 클릭. 이 페이지는 다이얼로그 없이 직접 저장 후 목록으로 리다이렉트됨",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"script": "(() => { window.__e2e_urlBeforeSave = window.location.href; return 'saved url: ' + window.__e2e_urlBeforeSave; })()",
|
||||
"description": "저장 전 URL 기록"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "저장",
|
||||
"description": "저장 버튼 클릭"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"duration": 2000,
|
||||
"description": "저장 처리 대기"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { window.__e2e_urlBeforeSave = window.location.href; const saveBtn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'); if(saveBtn){ saveBtn.click(); await new Promise(r=>setTimeout(r,2000)); return 'saved, url=' + window.location.href; } return 'save button not found'; })()",
|
||||
"expected": "저장 완료 후 목록 페이지로 리다이렉트"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "필수 검증 #2: 저장 완료 확인",
|
||||
"description": "저장 후 URL 변경 및 에러 여부 확인 (다이얼로그 없이 직접 저장 방식)",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(() => { const url = window.location.href; const isListPage = url.includes('/accounting/vendors') && !url.includes('mode='); const hasError = document.body.innerText.includes('404') || document.body.innerText.includes('500') || document.body.innerText.includes('Not Found'); const urlChanged = url !== window.__e2e_urlBeforeSave; return JSON.stringify({ url, isListPage, hasError, urlChanged, result: isListPage && !hasError ? 'PASS' : 'FAIL' }); })()",
|
||||
"description": "저장 후 목록 페이지 복귀 및 에러 없음 확인"
|
||||
}
|
||||
],
|
||||
"expected": "목록 페이지로 복귀, 에러 없음"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "수정 결과 확인 - 목록에서 검증",
|
||||
"description": "목록 페이지에서 수정된 거래처명 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(() => { const found = document.body.innerText.includes('수정테스트'); const rows = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ modifiedVisible: found, rowCount: rows, result: found ? 'PASS: 수정된 데이터 목록에 반영' : 'WARN: 수정 텍스트 미표시 (페이지네이션 또는 정렬 영향)' }); })()",
|
||||
"description": "목록에서 수정된 거래처 확인"
|
||||
}
|
||||
],
|
||||
"expected": "수정된 거래처명이 목록에 표시"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "원래 값 복원 - 수정된 거래처 클릭",
|
||||
"description": "수정된 거래처를 찾아 클릭하여 상세 페이지 진입",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const rows = document.querySelectorAll('table tbody tr'); let target = null; rows.forEach(row => { if(row.innerText.includes('수정테스트')) target = row; }); if(target){ target.click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked modified vendor, url=' + window.location.href; } rows[0]?.click(); await new Promise(r=>setTimeout(r,2000)); return 'modified vendor not found in current page, clicked first row. url=' + window.location.href; })()",
|
||||
"description": "수정된 거래처 행 클릭"
|
||||
}
|
||||
],
|
||||
"expected": "수정된 거래처 상세 페이지 진입"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "원래 값 복원 - 수정 버튼 클릭",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정'); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,1500)); return 'edit mode, url=' + window.location.href; } return 'edit button not found'; })()",
|
||||
"description": "수정 모드 진입"
|
||||
}
|
||||
],
|
||||
"expected": "수정 모드로 전환"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "원래 값 복원 - 거래처명 원복",
|
||||
"description": "거래처명에서 ' (수정테스트)' 제거",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const inputs = document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])'); let restored = false; inputs.forEach(inp => { if(inp.value.includes('수정테스트')){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const newVal = inp.value.replace(' (수정테스트)',''); nset.call(inp, newVal); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); restored = true; } }); return restored ? 'restored' : 'no field with 수정테스트 found'; })()",
|
||||
"description": "접미사 제거하여 원래 값 복원"
|
||||
}
|
||||
],
|
||||
"expected": "거래처명에서 ' (수정테스트)' 제거"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "원래 값 복원 - 저장",
|
||||
"description": "복원 저장 (다이얼로그 없이 직접 저장)",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,2000)); return 'saved, url=' + window.location.href; } return 'save button not found'; })()",
|
||||
"description": "저장 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"expected": "원래 값으로 복원 완료, 목록으로 리다이렉트"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "원래 값 복원 - 완료 확인",
|
||||
"description": "복원 후 목록 페이지에서 수정테스트 텍스트 제거 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "evaluate",
|
||||
"action": "evaluate",
|
||||
"script": "(() => { const url = window.location.href; const isListPage = url.includes('/accounting/vendors') && !url.includes('mode='); const stillModified = document.body.innerText.includes('수정테스트'); return JSON.stringify({ url, isListPage, stillModified, result: isListPage && !stillModified ? 'PASS: 원복 완료' : 'WARN: 원복 확인 필요' }); })()",
|
||||
"description": "목록에서 원복 확인"
|
||||
}
|
||||
],
|
||||
"expected": "수정테스트 텍스트 제거됨"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -71,24 +71,8 @@
|
||||
"id": 1,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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 'sidebar expanded';})()",
|
||||
"expect": {
|
||||
"sidebarReady": true
|
||||
}
|
||||
@@ -97,26 +81,9 @@
|
||||
"id": 2,
|
||||
"name": "출금관리 메뉴 진입",
|
||||
"description": "회계관리 > 출금관리 메뉴로 이동 (scrollAndFind 패턴 사용)",
|
||||
"menuNavigation": {
|
||||
"useEnhanced": true,
|
||||
"scrollAndFind": {
|
||||
"level1": {
|
||||
"text": "회계관리",
|
||||
"scrollUntilVisible": true,
|
||||
"clickToExpand": true,
|
||||
"waitAfterClick": 500
|
||||
},
|
||||
"level2": {
|
||||
"text": "출금관리",
|
||||
"scrollUntilVisible": true,
|
||||
"waitAfterClick": 300
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"useDirectUrl": true,
|
||||
"url": "/ko/accounting/withdrawals"
|
||||
}
|
||||
},
|
||||
"action": "menu_navigate",
|
||||
"level1": "회계관리",
|
||||
"level2": "출금관리",
|
||||
"expect": {
|
||||
"url": "/accounting/withdrawals",
|
||||
"visible": [
|
||||
@@ -129,6 +96,8 @@
|
||||
"id": 3,
|
||||
"name": "목록 페이지 구조 확인",
|
||||
"description": "테이블 및 필터 요소 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"visible": [
|
||||
"출금일",
|
||||
@@ -159,13 +128,8 @@
|
||||
"id": 4,
|
||||
"name": "계정과목명 드롭다운 옵션 확인",
|
||||
"description": "계정과목명 일괄변경 드롭다운 옵션 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "계정과목명 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"options": [
|
||||
"미설정",
|
||||
@@ -190,28 +154,8 @@
|
||||
"id": 5,
|
||||
"name": "체크박스 선택 후 계정과목명 일괄변경",
|
||||
"description": "테이블 행 선택 후 계정과목명 일괄변경 저장",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "첫 번째 행 체크박스",
|
||||
"description": "행 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "계정과목명 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "매입대금",
|
||||
"description": "매입대금 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "저장",
|
||||
"description": "저장 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const cb=document.querySelector('table tbody tr input[type=\"checkbox\"]');if(cb){cb.click();await new Promise(r=>setTimeout(r,500));}const dd=Array.from(document.querySelectorAll('button,select,[role=\"combobox\"]')).find(el=>el.innerText?.includes('계정과목명')||el.getAttribute('aria-label')?.includes('계정과목명'));if(dd){dd.click();await new Promise(r=>setTimeout(r,500));}const opt=Array.from(document.querySelectorAll('[role=\"option\"],li,button')).find(el=>el.innerText?.trim()==='매입대금');if(opt){opt.click();await new Promise(r=>setTimeout(r,500));}const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장');if(saveBtn){saveBtn.click();await new Promise(r=>setTimeout(r,1000));}return 'batch update attempted';})()",
|
||||
"expect": {
|
||||
"dialog": "확인 다이얼로그 표시",
|
||||
"dialogMessage": "1개의 출금 유형을 매입대금(으)로 모두 변경하시겠습니까?",
|
||||
@@ -225,9 +169,11 @@
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "⚠️ 필수 검증: 계정과목명 변경 데이터 반영 확인",
|
||||
"name": "필수 검증: 계정과목명 변경 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!",
|
||||
"description": "저장 후 테이블에서 변경된 출금유형 값 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"tableCell": {
|
||||
"row": 1,
|
||||
@@ -247,13 +193,7 @@
|
||||
"id": 7,
|
||||
"name": "출금 상세 페이지 이동",
|
||||
"description": "테이블 행 클릭하여 상세 페이지로 이동",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "테이블 첫 번째 행",
|
||||
"description": "행 클릭 (체크박스 제외 영역)"
|
||||
}
|
||||
],
|
||||
"action": "click_first_row",
|
||||
"expect": {
|
||||
"url": "/accounting/withdrawals/{id}",
|
||||
"visible": [
|
||||
@@ -269,6 +209,8 @@
|
||||
"id": 8,
|
||||
"name": "상세 페이지 읽기 모드 필드 확인",
|
||||
"description": "수정 전 필드들이 비활성화 상태인지 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"fields": [
|
||||
{
|
||||
@@ -306,7 +248,8 @@
|
||||
"id": 9,
|
||||
"name": "수정 모드 전환",
|
||||
"description": "수정 버튼 클릭하여 편집 모드로 전환",
|
||||
"click": "수정",
|
||||
"action": "click_if_exists",
|
||||
"target": "수정",
|
||||
"expect": {
|
||||
"url": "/accounting/withdrawals/{id}?mode=edit",
|
||||
"visible": [
|
||||
@@ -325,6 +268,8 @@
|
||||
"id": 10,
|
||||
"name": "수정 모드 필드 활성화 검증",
|
||||
"description": "수정 가능한 필드와 불가능한 필드 확인",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"fields": [
|
||||
{
|
||||
@@ -371,13 +316,8 @@
|
||||
"id": 11,
|
||||
"name": "거래처 드롭다운 옵션 확인",
|
||||
"description": "거래처 선택 드롭다운 옵션 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "거래처 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"options": [
|
||||
"거래처테스트",
|
||||
@@ -393,13 +333,8 @@
|
||||
"id": 12,
|
||||
"name": "출금 유형 드롭다운 옵션 확인",
|
||||
"description": "출금 유형 선택 드롭다운 옵션 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "출금 유형 드롭다운",
|
||||
"description": "드롭다운 열기"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"options": [
|
||||
"미설정",
|
||||
@@ -423,43 +358,15 @@
|
||||
"id": 13,
|
||||
"name": "수정 데이터 입력",
|
||||
"description": "수정 가능한 필드에 테스트 데이터 입력",
|
||||
"form": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "적요",
|
||||
"type": "text",
|
||||
"value": "테스트 적요 수정"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "거래처 드롭다운",
|
||||
"description": "거래처 드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "거래처테스트",
|
||||
"description": "거래처 선택"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "출금 유형 드롭다운",
|
||||
"description": "출금 유형 드롭다운 열기"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "매입대금",
|
||||
"description": "매입대금 선택"
|
||||
}
|
||||
]
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const inputs=document.querySelectorAll('input,textarea');const memo=Array.from(inputs).find(el=>el.getAttribute('name')?.includes('적요')||el.getAttribute('placeholder')?.includes('적요')||el.closest('[class*=\"memo\"],label')?.innerText?.includes('적요'));if(memo){memo.focus();memo.value='';memo.dispatchEvent(new Event('input',{bubbles:true}));memo.value='테스트 적요 수정';memo.dispatchEvent(new Event('input',{bubbles:true}));memo.dispatchEvent(new Event('change',{bubbles:true}));await new Promise(r=>setTimeout(r,500));}const vendorDD=Array.from(document.querySelectorAll('button,[role=\"combobox\"]')).find(el=>el.innerText?.includes('거래처')||el.getAttribute('aria-label')?.includes('거래처'));if(vendorDD){vendorDD.click();await new Promise(r=>setTimeout(r,500));const vendorOpt=Array.from(document.querySelectorAll('[role=\"option\"],li')).find(el=>el.innerText?.trim()==='거래처테스트');if(vendorOpt){vendorOpt.click();await new Promise(r=>setTimeout(r,500));}}const typeDD=Array.from(document.querySelectorAll('button,[role=\"combobox\"]')).find(el=>el.innerText?.includes('출금 유형')||el.getAttribute('aria-label')?.includes('출금'));if(typeDD){typeDD.click();await new Promise(r=>setTimeout(r,500));const typeOpt=Array.from(document.querySelectorAll('[role=\"option\"],li')).find(el=>el.innerText?.trim()==='매입대금');if(typeOpt){typeOpt.click();await new Promise(r=>setTimeout(r,500));}}return 'form filled';})()"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "저장 및 결과 확인",
|
||||
"description": "저장 버튼 클릭 후 데이터 반영 확인",
|
||||
"click": "저장",
|
||||
"action": "click_if_exists",
|
||||
"target": "저장",
|
||||
"expect": {
|
||||
"toast": "저장 완료 메시지",
|
||||
"url": "/accounting/withdrawals/{id}",
|
||||
@@ -473,9 +380,15 @@
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "⚠️ 필수 검증: 수정 데이터 반영 확인",
|
||||
"name": "필수 검증: 수정 데이터 반영 확인",
|
||||
"note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!",
|
||||
"description": "저장 후 상세 페이지에서 변경된 값 확인",
|
||||
"action": "verify_detail",
|
||||
"checks": [
|
||||
"적요: 테스트 적요 수정",
|
||||
"거래처: 거래처테스트",
|
||||
"출금 유형: 매입대금"
|
||||
],
|
||||
"expect": {
|
||||
"fields": [
|
||||
{
|
||||
@@ -497,18 +410,8 @@
|
||||
"id": 16,
|
||||
"name": "취소 버튼 동작 확인",
|
||||
"description": "수정 모드에서 취소 버튼 동작 검증",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "수정",
|
||||
"description": "수정 모드 진입"
|
||||
},
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "취소",
|
||||
"description": "취소 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const editBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정');if(editBtn){editBtn.click();await new Promise(r=>setTimeout(r,1000));}const cancelBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='취소');if(cancelBtn){cancelBtn.click();await new Promise(r=>setTimeout(r,1000));}return 'cancel tested';})()",
|
||||
"expect": {
|
||||
"url": "/accounting/withdrawals/{id}",
|
||||
"mode": "view",
|
||||
@@ -524,7 +427,8 @@
|
||||
"id": 17,
|
||||
"name": "목록 버튼 동작 확인",
|
||||
"description": "목록 버튼 클릭하여 목록 페이지로 이동",
|
||||
"click": "목록",
|
||||
"action": "click_if_exists",
|
||||
"target": "목록",
|
||||
"expect": {
|
||||
"url": "/accounting/withdrawals",
|
||||
"visible": [
|
||||
@@ -538,6 +442,8 @@
|
||||
"name": "필터 드롭다운 검증",
|
||||
"description": "목록 페이지 필터 드롭다운 옵션 확인",
|
||||
"note": "3개의 필터 드롭다운 존재 (거래처, 출금유형, 정렬)",
|
||||
"action": "verify_element",
|
||||
"target": "body",
|
||||
"expect": {
|
||||
"filters": [
|
||||
{
|
||||
@@ -565,13 +471,8 @@
|
||||
"id": 19,
|
||||
"name": "날짜 필터 검증",
|
||||
"description": "날짜 필터 버튼 동작 확인",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"action": "click_if_exists",
|
||||
"target": "당해년도",
|
||||
"description": "당해년도 버튼 클릭"
|
||||
}
|
||||
],
|
||||
"expect": {
|
||||
"dateRange": {
|
||||
"start": "2026-01-01",
|
||||
@@ -584,19 +485,14 @@
|
||||
"name": "페이지네이션 동작 확인",
|
||||
"description": "페이지네이션 버튼 동작 검증 (데이터 존재 시)",
|
||||
"condition": "데이터가 20건 이상인 경우에만 실행",
|
||||
"action": "click_if_exists",
|
||||
"target": "다음",
|
||||
"expect": {
|
||||
"pagination": {
|
||||
"itemsPerPage": 20,
|
||||
"currentPage": 1
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click_if_exists",
|
||||
"target": "다음",
|
||||
"description": "다음 페이지로 이동"
|
||||
}
|
||||
],
|
||||
"expectAfterAction": {
|
||||
"currentPage": 2
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user