diff --git a/hr-salary-long-term-care.json b/hr-salary-long-term-care.json index 2dc3fe2..c05c178 100644 --- a/hr-salary-long-term-care.json +++ b/hr-salary-long-term-care.json @@ -1,7 +1,7 @@ { "id": "hr-salary-long-term-care", "name": "급여 장기요양보험 필드 검증 테스트", - "version": "1.1.0", + "version": "1.2.0", "enabled": true, "screenshotPolicy": { "captureOnFail": true, @@ -68,14 +68,14 @@ "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);})()" + "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 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);})()" + "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, @@ -104,14 +104,14 @@ "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);})()" + "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 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);})()", + "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 }, { diff --git a/settings-calendar-crud.json b/settings-calendar-crud.json index c05afeb..a9ec840 100644 --- a/settings-calendar-crud.json +++ b/settings-calendar-crud.json @@ -1,13 +1,13 @@ { "id": "settings-calendar-crud", "name": "달력 일정 CRUD 테스트", - "version": "3.2.0", + "version": "3.3.0", "enabled": true, "screenshotPolicy": { "captureOnFail": true, "captureOnPass": false }, - "description": "설정 > 달력관리 메뉴의 달력 일정 등록/조회/수정/삭제 전체 CRUD (Server Actions POST 방식)", + "description": "설정 > 달력관리 메뉴의 달력 일정 등록/조회/수정/삭제 전체 CRUD (Server Actions POST 방식, 토스트 미사용)", "baseUrl": "https://dev.codebridge-x.com", "menuNavigation": { "level1": "설정", @@ -66,27 +66,20 @@ }, { "id": 7, - "name": "[CREATE] 등록 토스트 확인", - "phase": "CREATE", - "action": "verify_toast", - "verify": { "contains": "등록|완료|성공" } - }, - { - "id": 8, "name": "[CREATE] API POST 검증 (Server Action)", "phase": "CREATE", "action": "evaluate", "script": "(()=>{try{const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[]};const logs=apiData.logs||[];const posts=logs.filter(l=>l.method==='POST'&&(l.url.includes('calendar')||l.url.includes('schedule'))&&l.status>=200&&l.status<300);if(posts.length>0){return JSON.stringify({ok:true,info:'pass: ServerAction POST ×'+posts.length+' status='+posts[posts.length-1].status});}const allPosts=logs.filter(l=>l.method==='POST'&&l.ok);if(allPosts.length>0){return JSON.stringify({ok:true,info:'pass: POST calls='+allPosts.length+' (ServerAction pattern)'});}return JSON.stringify({ok:true,info:'warn: no POST captured (apiLogs='+logs.length+')'});}catch(e){return JSON.stringify({ok:true,info:'warn: API check error: '+e.message});}})()" }, { - "id": 9, + "id": 8, "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 R={phase:'MODAL_CHECK'};await w(1000);const modal=document.querySelector('[role=\"dialog\"]');if(isVis(modal)){const closeBtn=Array.from(modal.querySelectorAll('button')).find(b=>/닫기|취소|Close/.test(b.innerText?.trim()));if(closeBtn){closeBtn.click();await w(500);}R.wasClosed=true;}R.ok=true;return JSON.stringify(R);})()" }, { - "id": 10, + "id": 9, "name": "[CREATE] 목록에서 등록 결과 확인", "phase": "CREATE", "action": "evaluate", @@ -94,7 +87,7 @@ "timeout": 10000 }, { - "id": 11, + "id": 10, "name": "[UPDATE] E2E 일정 행 클릭 → 다이얼로그 열기 → 수정 → 저장 (통합)", "phase": "UPDATE", "action": "evaluate", @@ -102,29 +95,21 @@ "timeout": 25000 }, { - "id": 12, - "name": "[UPDATE] 수정 토스트 확인 (Server Action: 토스트 없을 수 있음)", - "phase": "UPDATE", - "action": "verify_toast", - "verify": { "contains": "수정|완료|성공" }, - "critical": false - }, - { - "id": 13, + "id": 11, "name": "[UPDATE] API 수정 검증 (Server Action)", "phase": "UPDATE", "action": "evaluate", "script": "(()=>{try{const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[]};const logs=apiData.logs||[];const posts=logs.filter(l=>l.method==='POST'&&(l.url.includes('calendar')||l.url.includes('schedule'))&&l.ok);return JSON.stringify({ok:true,info:'pass: ServerAction calls='+posts.length+' total='+logs.length});}catch(e){return JSON.stringify({ok:true,info:'warn: '+e.message});}})()" }, { - "id": 14, + "id": 12, "name": "[UPDATE] 모달 닫힘 확인", "phase": "UPDATE", "action": "evaluate", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const isVis=el=>!!el&&el.getBoundingClientRect().width>0;await w(500);const modal=document.querySelector('[role=\"dialog\"],[data-state=\"open\"][class*=\"Sheet\"]');if(isVis(modal)){const closeBtn=Array.from(modal.querySelectorAll('button')).find(b=>/닫기|취소|Close/.test(b.innerText?.trim()));if(closeBtn){closeBtn.click();await w(500);}}return JSON.stringify({ok:true,phase:'MODAL_CLOSE'});})()" }, { - "id": 15, + "id": 13, "name": "[DELETE] E2E 수정일정 행 클릭 → 삭제 (통합)", "phase": "DELETE", "action": "evaluate", @@ -133,22 +118,14 @@ "critical": false }, { - "id": 16, - "name": "[DELETE] 삭제 토스트 확인 (Server Action: 토스트 없을 수 있음)", - "phase": "DELETE", - "action": "verify_toast", - "verify": { "contains": "삭제|완료|성공" }, - "critical": false - }, - { - "id": 17, + "id": 14, "name": "[DELETE] API 삭제 검증 (Server Action)", "phase": "DELETE", "action": "evaluate", "script": "(()=>{try{const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[]};const logs=apiData.logs||[];const posts=logs.filter(l=>l.method==='POST'&&(l.url.includes('calendar')||l.url.includes('schedule'))&&l.ok);return JSON.stringify({ok:true,info:'pass: ServerAction calls='+posts.length+' total='+logs.length});}catch(e){return JSON.stringify({ok:true,info:'warn: '+e.message});}})()" }, { - "id": 18, + "id": 15, "name": "[DELETE] 목록에서 삭제 확인", "phase": "DELETE", "action": "evaluate", @@ -156,7 +133,7 @@ "timeout": 10000 }, { - "id": 19, + "id": 16, "name": "[SUMMARY] API 호출 통계", "action": "evaluate", "script": "(()=>{try{const apiData=window.__E2E__?window.__E2E__.getApiLogs():{logs:[]};const logs=apiData.logs||[];const cal=logs.filter(l=>l.url.includes('calendar')||l.url.includes('schedule'));return JSON.stringify({ok:true,info:'API total='+logs.length+' calendar='+cal.length+' POST='+cal.filter(l=>l.method==='POST').length+' success='+cal.filter(l=>l.ok).length});}catch(e){return JSON.stringify({ok:true,info:'API summary error: '+e.message});}})()"