Files
sam-scenarios/hr-salary-long-term-care.json
김보곤 b95f7fc132 fix: settings-calendar-crud, hr-salary-long-term-care 안정성 개선
settings-calendar-crud v3.2.0:
- UPDATE steps 11+12 통합: 행 클릭→다이얼로그 열기→수정→저장을 단일 스텝으로 병합
  (스텝 간 다이얼로그 소실 방지, 3회 재시도 + waitDlg 폴링)
- DELETE 스텝: waitDlg 폴링 방식으로 다이얼로그 감지 강화
- 다이얼로그 셀렉터 확장: data-state=open, Sheet, DialogContent 추가
- toast 검증 스텝에 critical:false 추가 (Server Action 토스트 미표시 대응)
- 전체 스텝 20→19로 축소

hr-salary-long-term-care v1.1.0:
- Step 12: 고정 2000ms 대기 → 8초 폴링 방식 (300ms 간격)으로 변경
  (step-executor 3초 기본 타임아웃 충돌 해결)
- Step 12 timeout: 5000→10000ms
- Step 15: offsetParent→getBoundingClientRect 가시성 검사, plain string→JSON 반환
- Step 16: plain string→JSON 반환 (json_fail 경고 해결)
- 다이얼로그 셀렉터 확장: data-state=open Sheet 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:46:14 +09:00

138 lines
10 KiB
JSON
Raw 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.1.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 labels=Array.from(scope.querySelectorAll('label,dt,[class*=\"label\"],[class*=\"Label\"]'));const ltcLabel=labels.find(l=>l.innerText?.includes('장기요양'));R.labelFound=!!ltcLabel;if(ltcLabel){const container=ltcLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');if(container){const valueEl=container.querySelector('input,span,dd,[class*=\"value\"],[class*=\"Value\"]');R.value=valueEl?.value||valueEl?.innerText?.trim()||'N/A';}}R.ok=R.hasLongTermCare||R.labelFound;R.info=R.ok?'pass: 장기요양보험 필드 발견':'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 labels=Array.from(scope.querySelectorAll('label,dt,[class*=\"label\"],[class*=\"Label\"]'));const getVal=keyword=>{const lbl=labels.find(l=>l.innerText?.includes(keyword));if(!lbl)return null;const container=lbl.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');if(!container)return null;const valEl=container.querySelector('input,span,dd,[class*=\"value\"]');const raw=valEl?.value||valEl?.innerText?.trim()||'';return parseInt(raw.replace(/[^0-9]/g,''))||null;};R.healthInsurance=getVal('건강보험');R.longTermCare=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 labels=Array.from(scope.querySelectorAll('label'));const ltcLabel=labels.find(l=>l.innerText?.includes('장기요양'));R.labelFound=!!ltcLabel;if(ltcLabel){const container=ltcLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');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 labels=Array.from(scope.querySelectorAll('label'));const healthLabel=labels.find(l=>l.innerText?.includes('건강보험')&&!l.innerText?.includes('장기'));if(!healthLabel){R.info='건강보험 입력 필드 미발견';R.ok=true;return JSON.stringify(R);}const healthContainer=healthLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');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 ltcLabel=labels.find(l=>l.innerText?.includes('장기요양'));if(ltcLabel){const ltcContainer=ltcLabel.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"]');const ltcEl=ltcContainer?.querySelector('input,span,[class*=\"value\"]');const ltcVal=ltcEl?.value||ltcEl?.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": "조회 위주 테스트, 등록 다이얼로그는 취소로 닫음. 데이터 변경 없음."
}
}