Files
sam-scenarios/hr-salary-long-term-care.json
김보곤 4b5bf7a873 fix: 급여 장기요양 셀렉터 수정(label→span) + 달력 토스트 스텝 제거
- hr-salary-long-term-care: steps 8,9,13,14 셀렉터를 label→span으로 변경, container를 [class*=field]→[class*=flex]로 변경 (실제 DOM 구조 반영)
- settings-calendar-crud: toast verify steps 3개 제거 (Server Actions는 토스트 미사용), 19→16 스텝
2026-03-05 21:51:41 +09:00

138 lines
10 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"id": "hr-salary-long-term-care",
"name": "급여 장기요양보험 필드 검증 테스트",
"version": "1.2.0",
"enabled": true,
"screenshotPolicy": {
"captureOnFail": true,
"captureOnPass": false
},
"description": "인사관리 > 급여관리의 장기요양보험 필드 존재 확인 + 자동계산(건강보험×12.81%) 검증",
"baseUrl": "https://dev.codebridge-x.com",
"menuNavigation": {
"level1": "인사관리",
"level2": "급여관리",
"expectedUrl": "/hr/salary-management",
"searchWithinParent": true,
"closeOtherMenus": true
},
"auth": { "username": "TestUser5", "password": "password123!" },
"steps": [
{
"id": 1,
"name": "메뉴 진입: 인사관리 > 급여관리",
"action": "menu_navigate",
"level1": "인사관리",
"level2": "급여관리",
"expected": { "url_contains": "/hr/salary" }
},
{
"id": 2,
"name": "페이지 로드 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 3,
"name": "URL 검증",
"action": "verify_url",
"expected": { "url_contains": "/hr/salary-management" }
},
{
"id": 4,
"name": "목업 감지",
"action": "verify_not_mockup",
"checks": ["급여 목록 표시", "등록 버튼 존재"]
},
{
"id": 5,
"name": "테이블 로드 대기",
"action": "wait_for_table",
"timeout": 15000
},
{
"id": 6,
"name": "[READ] 첫 번째 행 클릭 (상세 다이얼로그 열기)",
"phase": "READ",
"action": "click_first_row"
},
{
"id": 7,
"name": "[READ] 상세 다이얼로그 대기",
"phase": "READ",
"action": "wait",
"timeout": 2000
},
{
"id": 8,
"name": "[READ] 장기요양보험 필드 존재 확인 (상세)",
"phase": "READ",
"action": "evaluate",
"script": "(()=>{const R={phase:'DETAIL_LTC_CHECK'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const bodyText=scope.innerText||'';R.hasLongTermCare=bodyText.includes('장기요양')||bodyText.includes('장기요양보험');const allEls=Array.from(scope.querySelectorAll('span,label,dt'));const ltcEl=allEls.find(l=>/^장기요양(보험)?$/.test(l.innerText?.trim()));R.labelFound=!!ltcEl;if(ltcEl){const container=ltcEl.closest('[class*=\"flex\"]')||ltcEl.parentElement;if(container){const valSpans=Array.from(container.querySelectorAll('span,div')).filter(el=>el!==ltcEl&&/[0-9]/.test(el.innerText||''));if(valSpans.length>0)R.value=valSpans[valSpans.length-1].innerText?.trim()||'N/A';}}R.ok=R.hasLongTermCare||R.labelFound;R.info=R.ok?'pass: 장기요양보험 필드 발견'+(R.value?' ('+R.value+')':''):'fail: 장기요양보험 필드 미발견';return JSON.stringify(R);})()"
},
{
"id": 9,
"name": "[READ] 건강보험/장기요양 값 비교 (자동계산 검증)",
"phase": "READ",
"action": "evaluate",
"script": "(()=>{const R={phase:'CALC_VERIFY'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const allEls=Array.from(scope.querySelectorAll('span,label,dt'));const getVal=keyword=>{const lbl=allEls.find(l=>l.innerText?.trim()===keyword);if(!lbl)return null;const container=lbl.closest('[class*=\"flex\"]')||lbl.parentElement;if(!container)return null;const valEls=Array.from(container.querySelectorAll('span,div')).filter(el=>el!==lbl&&/[0-9]/.test(el.innerText||''));if(valEls.length===0)return null;const raw=valEls[valEls.length-1].innerText?.trim()||'';return parseInt(raw.replace(/[^0-9]/g,''))||null;};R.healthInsurance=getVal('건강보험');R.longTermCare=getVal('장기요양보험')||getVal('장기요양');if(R.healthInsurance&&R.longTermCare){const expected=Math.round(R.healthInsurance*0.1281);R.expectedLTC=expected;R.tolerance=Math.abs(R.longTermCare-expected);R.isCorrect=R.tolerance<=10;R.info=R.isCorrect?'pass: 장기요양='+R.longTermCare+' (건강보험 '+R.healthInsurance+'×12.81%='+expected+')':'warn: 장기요양='+R.longTermCare+' vs 예상='+expected+' (차이='+R.tolerance+')';}else{R.info='warn: 건강보험('+R.healthInsurance+') 또는 장기요양('+R.longTermCare+') 값 미확인';}R.ok=true;return JSON.stringify(R);})()"
},
{
"id": 10,
"name": "[READ] 상세 다이얼로그 닫기",
"phase": "READ",
"action": "close_modal_if_open"
},
{
"id": 11,
"name": "[CREATE] 등록 다이얼로그 열기",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(500);const R={phase:'CREATE_OPEN'};const btn=Array.from(document.querySelectorAll('button')).find(b=>/급여.*등록|등록|추가/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(btn){btn.click();await w(2000);R.clicked=true;}else{R.clicked=false;R.info='등록 버튼 미발견';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 12,
"name": "[CREATE] 등록 다이얼로그 대기 (폴링)",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'WAIT_DIALOG'};const end=Date.now()+8000;while(Date.now()<end){const dlg=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[data-state=\"open\"][class*=\"Sheet\"],[class*=\"DialogContent\"]');if(dlg&&dlg.getBoundingClientRect().width>0){R.ok=true;R.info='pass: dialog open (position:fixed safe)';R.dialogFound=true;R.visible=true;return JSON.stringify(R);}await w(300);}const dlg=document.querySelector('[role=\"dialog\"]');R.ok=true;R.info='warn: dialog not visible after 8s polling';R.dialogFound=!!dlg;R.visible=false;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 13,
"name": "[CREATE] 등록 폼에서 장기요양보험 필드 확인",
"phase": "CREATE",
"action": "evaluate",
"script": "(()=>{const R={phase:'CREATE_LTC_CHECK'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const bodyText=scope.innerText||'';R.hasLongTermCare=bodyText.includes('장기요양')||bodyText.includes('장기요양보험');const allEls=Array.from(scope.querySelectorAll('span,label,dt'));const ltcEl=allEls.find(l=>/^장기요양(보험)?$/.test(l.innerText?.trim()));R.labelFound=!!ltcEl;if(ltcEl){const container=ltcEl.closest('[class*=\"flex\"]')||ltcEl.parentElement;if(container){const input=container.querySelector('input');R.hasInput=!!input;R.inputReadOnly=input?.readOnly||false;R.inputValue=input?.value||'';}}R.ok=R.hasLongTermCare||R.labelFound;R.info=R.ok?'pass: 등록 폼에 장기요양보험 필드 존재'+(R.inputReadOnly?' (자동계산, readOnly)':' (입력 가능)'):'fail: 등록 폼에 장기요양보험 필드 미발견';return JSON.stringify(R);})()"
},
{
"id": 14,
"name": "[CREATE] 건강보험 입력 → 장기요양 자동계산 검증",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'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 R={phase:'AUTO_CALC'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');const scope=modal||document;const allEls=Array.from(scope.querySelectorAll('span,label,dt'));const healthEl=allEls.find(l=>l.innerText?.trim()==='건강보험');if(!healthEl){R.info='건강보험 입력 필드 미발견';R.ok=true;return JSON.stringify(R);}const healthContainer=healthEl.closest('[class*=\"flex\"]')||healthEl.parentElement;const healthInput=healthContainer?.querySelector('input');if(!healthInput||healthInput.readOnly||healthInput.disabled){R.info='건강보험 입력 불가 (readOnly)';R.ok=true;return JSON.stringify(R);}sv(healthInput,'100000');await w(800);const ltcEl=allEls.find(l=>/^장기요양(보험)?$/.test(l.innerText?.trim()));if(ltcEl){const ltcContainer=ltcEl.closest('[class*=\"flex\"]')||ltcEl.parentElement;const ltcInput=ltcContainer?.querySelector('input');const ltcValEl=ltcInput||Array.from(ltcContainer?.querySelectorAll('span,div')||[]).find(el=>el!==ltcEl&&/[0-9]/.test(el.innerText||''));const ltcVal=ltcInput?.value||ltcValEl?.innerText?.trim()||'';const numVal=parseInt(ltcVal.replace(/[^0-9]/g,''))||0;R.healthInput=100000;R.longTermCareOutput=numVal;R.expected=Math.round(100000*0.1281);R.isAutoCalculated=Math.abs(numVal-R.expected)<=10;R.info=R.isAutoCalculated?'pass: 건강보험 100,000 → 장기요양 '+numVal+' (예상 '+R.expected+')':'warn: 장기요양='+numVal+' vs 예상='+R.expected;}else{R.info='장기요양 필드 미발견';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 15,
"name": "[CREATE] 등록 다이얼로그 닫기 (데이터 저장 안함)",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const isVis=el=>!!el&&el.getBoundingClientRect().width>0;const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"],[data-state=\"open\"][class*=\"Sheet\"]');if(isVis(modal)){const cancelBtn=Array.from(modal.querySelectorAll('button')).find(b=>/취소|닫기|Close/.test(b.innerText?.trim()));if(cancelBtn){cancelBtn.click();await w(500);}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(500);}}return JSON.stringify({ok:true,info:'pass: dialog closed'});})()"
},
{
"id": 16,
"name": "[SUMMARY] API 호출 통계",
"action": "evaluate",
"script": "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().logs:[]);const salaryApi=logs.filter(l=>l.url.includes('salary')||l.url.includes('payroll'));return JSON.stringify({ok:true,info:'pass: API summary - total='+logs.length+' salary_api='+salaryApi.length+' success='+logs.filter(l=>l.status>=200&&l.status<300).length+' failed='+logs.filter(l=>l.status>=400).length});})()"
}
],
"expectedAPIs": [
{ "method": "GET", "endpoint": "/api/v1/payrolls", "description": "급여 목록 조회" },
{ "method": "GET", "endpoint": "/api/v1/payrolls/{id}", "description": "급여 상세 조회" }
],
"rollbackPlan": {
"note": "조회 위주 테스트, 등록 다이얼로그는 취소로 닫음. 데이터 변경 없음."
}
}