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:
김보곤
2026-02-28 17:21:01 +09:00
parent 21f2a72dee
commit 23827c257d
20 changed files with 634 additions and 2434 deletions

View File

@@ -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",
"target": "table tbody",
"text": "가우스",
"description": "테이블에 가우스 텍스트 존재 확인"
}
],
"action": "verify_text",
"target": "table tbody",
"text": "가우스",
"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",
"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": "구분 필터에서 매출 선택"
}
],
"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'; })()",
"expected": "매출 거래처만 필터링"
},
{
"id": 10,
"name": "구분 필터 초기화",
"description": "구분 필터를 '전체'로 되돌리기",
"actions": [
{
"type": "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": "구분 필터를 전체로 초기화"
}
],
"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'; })()",
"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",
"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": "첫 번째 편집 가능 필드(거래처명)에 접미사 추가"
}
],
"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'; })()",
"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",
"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": "저장 후 목록 페이지 복귀 및 에러 없음 확인"
}
],
"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' }); })()",
"expected": "목록 페이지로 복귀, 에러 없음"
},
{
"id": 27,
"name": "수정 결과 확인 - 목록에서 검증",
"description": "목록 페이지에서 수정된 거래처명 확인",
"actions": [
{
"type": "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": "목록에서 수정된 거래처 확인"
}
],
"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: 수정 텍스트 미표시 (페이지네이션 또는 정렬 영향)' }); })()",
"expected": "수정된 거래처명이 목록에 표시"
},
{
"id": 28,
"name": "원래 값 복원 - 수정된 거래처 클릭",
"description": "수정된 거래처를 찾아 클릭하여 상세 페이지 진입",
"actions": [
{
"type": "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": "수정된 거래처 행 클릭"
}
],
"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; })()",
"expected": "수정된 거래처 상세 페이지 진입"
},
{
"id": 29,
"name": "원래 값 복원 - 수정 버튼 클릭",
"actions": [
{
"type": "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": "수정 모드 진입"
}
],
"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'; })()",
"expected": "수정 모드로 전환"
},
{
"id": 30,
"name": "원래 값 복원 - 거래처명 원복",
"description": "거래처명에서 ' (수정테스트)' 제거",
"actions": [
{
"type": "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": "접미사 제거하여 원래 값 복원"
}
],
"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'; })()",
"expected": "거래처명에서 ' (수정테스트)' 제거"
},
{
"id": 31,
"name": "원래 값 복원 - 저장",
"description": "복원 저장 (다이얼로그 없이 직접 저장)",
"actions": [
{
"type": "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": "저장 버튼 클릭"
}
],
"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'; })()",
"expected": "원래 값으로 복원 완료, 목록으로 리다이렉트"
},
{
"id": 32,
"name": "원래 값 복원 - 완료 확인",
"description": "복원 후 목록 페이지에서 수정테스트 텍스트 제거 확인",
"actions": [
{
"type": "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": "목록에서 원복 확인"
}
],
"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: 원복 확인 필요' }); })()",
"expected": "수정테스트 텍스트 제거됨"
},
{
@@ -665,4 +490,4 @@
"description": "거래처 수정"
}
]
}
}