feat: 달력 CRUD, 수주 일괄삭제, 급여 장기요양보험 시나리오 추가
- settings-calendar-crud: 달력관리 일정 CRUD 전체 흐름 (v3.1.0) - Radix UI PointerEvent 탭 전환, position:fixed Sheet 다이얼로그 대응 - Server Actions POST 패턴 API 검증 - sales-order-bulk-delete: 수주관리 일괄삭제 기능 검증 (20 steps) - hr-salary-long-term-care: 급여관리 장기요양보험 필드/자동계산 검증 (16 steps) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
138
hr-salary-long-term-care.json
Normal file
138
hr-salary-long-term-care.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"id": "hr-salary-long-term-care",
|
||||
"name": "급여 장기요양보험 필드 검증 테스트",
|
||||
"version": "1.0.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));await w(2000);const dlg=document.querySelector('[role=\"dialog\"]');const isVis=dlg&&dlg.getBoundingClientRect().width>0;return JSON.stringify({ok:true,info:isVis?'pass: dialog open (position:fixed)':'warn: dialog not visible',dialogFound:!!dlg,visible:isVis});})()",
|
||||
"timeout": 5000
|
||||
},
|
||||
{
|
||||
"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 modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"Dialog\"]');if(modal&&modal.offsetParent!==null){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 'pass: dialog closed';})()"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "[SUMMARY] API 호출 통계",
|
||||
"action": "evaluate",
|
||||
"script": "(()=>{const logs=window.__API_LOGS__||[];const salaryApi=logs.filter(l=>l.url.includes('salary')||l.url.includes('payroll'));return '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": "조회 위주 테스트, 등록 다이얼로그는 취소로 닫음. 데이터 변경 없음."
|
||||
}
|
||||
}
|
||||
186
sales-order-bulk-delete.json
Normal file
186
sales-order-bulk-delete.json
Normal file
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"id": "sales-order-bulk-delete",
|
||||
"name": "수주 일괄삭제 테스트",
|
||||
"version": "1.0.0",
|
||||
"enabled": true,
|
||||
"screenshotPolicy": {
|
||||
"captureOnFail": true,
|
||||
"captureOnPass": false
|
||||
},
|
||||
"description": "판매관리 > 수주관리의 수주 일괄삭제(bulk delete) 기능 검증: 체크박스 선택 → 일괄삭제 → 확인 → API DELETE /orders/bulk",
|
||||
"baseUrl": "https://dev.codebridge-x.com",
|
||||
"menuNavigation": {
|
||||
"level1": "판매관리",
|
||||
"level2": "수주관리",
|
||||
"expectedUrl": "/sales/order-management-sales",
|
||||
"searchWithinParent": true,
|
||||
"closeOtherMenus": true
|
||||
},
|
||||
"auth": { "username": "TestUser5", "password": "password123!" },
|
||||
"testData": {
|
||||
"create1": { "siteName": "E2E_TEST_일괄1_{timestamp}" },
|
||||
"create2": { "siteName": "E2E_TEST_일괄2_{timestamp}" }
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "메뉴 진입: 판매관리 > 수주관리",
|
||||
"action": "menu_navigate",
|
||||
"level1": "판매관리",
|
||||
"level2": "수주관리",
|
||||
"expected": { "url_contains": "/sales/order" }
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "페이지 로드 대기",
|
||||
"action": "wait",
|
||||
"timeout": 3000
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "URL 검증",
|
||||
"action": "verify_url",
|
||||
"expected": { "url_contains": "/sales/order-management-sales" }
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "목업 감지",
|
||||
"action": "verify_not_mockup",
|
||||
"checks": ["수주 목록 표시", "등록 버튼 존재"]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "테이블 로드 대기",
|
||||
"action": "wait_for_table",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "[CREATE-1] ts 초기화 + 등록 버튼 클릭",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const n=new Date();const p=v=>v.toString().padStart(2,'0');const ts=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:'CREATE1_OPEN',ts};const btn=Array.from(document.querySelectorAll('button')).find(b=>/수주.*등록|등록/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!btn){R.error='등록 버튼 없음';R.ok=false;return JSON.stringify(R);}btn.click();await w(2500);R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "[CREATE-1] 수주처 콤보박스 선택",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'COMBO1'};const formArea=document.querySelector('main,[class*=\"content\"]')||document.body;const combos=Array.from(formArea.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null&&!b.closest('nav,[class*=sidebar]'));if(combos.length>0){combos[0].click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opts=lb.querySelectorAll('[role=\"option\"]');if(opts.length>0){opts[0].click();await w(400);R.selected=true;}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "[CREATE-1] 현장명 입력",
|
||||
"phase": "CREATE",
|
||||
"action": "fill_form",
|
||||
"fields": [
|
||||
{ "name": "현장명", "value": "E2E_TEST_일괄1_{timestamp}" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "[CREATE-1] 품목 추가 + 최소 입력",
|
||||
"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:'ITEM1'};const addBtn=Array.from(document.querySelectorAll('button')).find(b=>(b.innerText?.trim()==='+'||b.innerText?.includes('추가'))&&b.offsetParent!==null&&!b.disabled);if(addBtn){addBtn.click();await w(1000);}const inputs=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('수량')||i.name?.includes('qty')||i.name?.includes('quantity');});if(qtyInput){sv(qtyInput,'1');await w(200);}const priceInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('단가')||i.name?.includes('price');});if(priceInput){sv(priceInput,'10000');await w(200);}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "[CREATE-1] 저장",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const btn=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(btn){btn.click();await w(3000);}return 'pass: save clicked';})()",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "[CREATE-1] 토스트 확인 + 목록 복귀",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const onForm=location.search.includes('mode=new')||location.search.includes('mode=edit');if(onForm){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);}}return 'pass: returned to list';})()",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "[CREATE-2] 두번째 수주 등록",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'CREATE2'};const btn=Array.from(document.querySelectorAll('button')).find(b=>/수주.*등록|등록/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!btn){R.info='등록 버튼 없음';R.ok=true;return JSON.stringify(R);}btn.click();await w(2500);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 combos=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null&&!b.closest('nav,[class*=sidebar]'));if(combos.length>0){combos[0].click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opts=lb.querySelectorAll('[role=\"option\"]');if(opts.length>0){opts[0].click();await w(400);}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}const ts=window.__E2E_TS__||'';const siteInput=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled).find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||'';return lbl.includes('현장');});if(siteInput)sv(siteInput,'E2E_TEST_일괄2_'+ts);await w(300);const addBtn=Array.from(document.querySelectorAll('button')).find(b=>(b.innerText?.trim()==='+'||b.innerText?.includes('추가'))&&b.offsetParent!==null&&!b.disabled);if(addBtn){addBtn.click();await w(1000);}const inputs=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('수량')||i.name?.includes('qty');});if(qtyInput)sv(qtyInput,'1');await w(200);const priceInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('단가')||i.name?.includes('price');});if(priceInput)sv(priceInput,'10000');await w(200);const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(saveBtn){saveBtn.click();await w(3000);}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "[CREATE-2] 목록 복귀",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const onForm=location.search.includes('mode=new')||location.search.includes('mode=edit');if(onForm){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);}}return 'pass: returned to list';})()",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "[BULK-DELETE] 목록 안정화 대기",
|
||||
"phase": "DELETE",
|
||||
"action": "wait",
|
||||
"timeout": 3000
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "[BULK-DELETE] 체크박스 UI 확인 + E2E 행 선택",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHECKBOX_SELECT'};const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);const e2eRows=rows.filter(r=>r.innerText?.includes('E2E_TEST_일괄'));R.e2eRowCount=e2eRows.length;let selected=0;for(const row of e2eRows){const cb=row.querySelector('input[type=\"checkbox\"],button[role=\"checkbox\"],[data-state]');if(cb){cb.click();await w(300);selected++;}}R.selected=selected;if(selected===0&&rows.length>0){const allCb=document.querySelector('thead input[type=\"checkbox\"],thead button[role=\"checkbox\"],th input[type=\"checkbox\"],th button[role=\"checkbox\"]');if(allCb){R.info='E2E 행 미발견, 전체 선택 확인만';R.hasSelectAll=true;}else{R.info='체크박스 UI 미발견';}}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "[BULK-DELETE] 일괄삭제 버튼 클릭",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'BULK_DELETE_CLICK'};const delBtn=Array.from(document.querySelectorAll('button')).find(b=>{const t=b.innerText?.trim()||'';return(t.includes('삭제')||t.includes('일괄')||t.includes('완전삭제'))&&b.offsetParent!==null&&!b.disabled;});if(!delBtn){R.info='일괄삭제 버튼 미발견 (선택 필요)';R.ok=true;return JSON.stringify(R);}delBtn.click();await w(1500);R.clicked=true;R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "[BULK-DELETE] 확인 다이얼로그 처리",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CONFIRM_DELETE'};const dialog=document.querySelector('[role=\"alertdialog\"],[role=\"dialog\"]');if(dialog&&dialog.offsetParent!==null){const cfmBtn=Array.from(dialog.querySelectorAll('button')).find(b=>/삭제|확인|예/.test(b.innerText?.trim()));if(cfmBtn){cfmBtn.click();await w(3000);R.confirmed=true;}else{R.info='확인 버튼 미발견';}}else{R.info='다이얼로그 미표시';}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 15000,
|
||||
"critical": false
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "[BULK-DELETE] API DELETE /orders/bulk 검증",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(()=>{const logs=window.__API_LOGS__||[];const bulkDels=logs.filter(l=>l.method==='DELETE'&&(l.url.includes('bulk')||l.url.includes('orders'))&&l.status>=200&&l.status<300);const singleDels=logs.filter(l=>l.method==='DELETE'&&l.status>=200&&l.status<300);return bulkDels.length>0?'pass: BULK DELETE '+bulkDels[bulkDels.length-1].status+' url='+bulkDels[bulkDels.length-1].url.split('/').slice(-2).join('/'):singleDels.length>0?'pass: DELETE found ('+singleDels.length+' calls)':'warn: no DELETE API calls found';})()"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "[BULK-DELETE] 목록에서 E2E 데이터 제거 확인",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'IMPOSSIBLE';const R={phase:'VERIFY_DELETED'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const remaining=rows.filter(r=>r.innerText?.includes('E2E_TEST_일괄')&&r.innerText?.includes(ts));R.remaining=remaining.length;R.ok=remaining.length===0;R.info=remaining.length===0?'pass: E2E bulk data removed':'warn: '+remaining.length+' E2E rows still exist';return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "[SUMMARY] API 호출 통계",
|
||||
"action": "evaluate",
|
||||
"script": "(()=>{const logs=window.__API_LOGS__||[];const summary={total:logs.length,GET:logs.filter(l=>l.method==='GET').length,POST:logs.filter(l=>l.method==='POST').length,PUT:logs.filter(l=>l.method==='PUT').length,DELETE:logs.filter(l=>l.method==='DELETE').length,success:logs.filter(l=>l.status>=200&&l.status<300).length,failed:logs.filter(l=>l.status>=400).length,avgResponseTime:logs.length>0?Math.round(logs.reduce((s,l)=>s+(l.duration||0),0)/logs.length):0};return 'pass: API summary - total='+summary.total+' POST='+summary.POST+' DELETE='+summary.DELETE+' success='+summary.success+' failed='+summary.failed+' avg='+summary.avgResponseTime+'ms';})()"
|
||||
}
|
||||
],
|
||||
"expectedAPIs": [
|
||||
{ "method": "GET", "endpoint": "/api/v1/orders", "description": "수주 목록 조회" },
|
||||
{ "method": "POST", "endpoint": "/api/v1/orders", "description": "수주 등록 (2건)" },
|
||||
{ "method": "DELETE", "endpoint": "/api/v1/orders/bulk", "description": "수주 일괄삭제" }
|
||||
],
|
||||
"rollbackPlan": {
|
||||
"onCreateFail": "등록 실패 시 테스트 종료",
|
||||
"onDeleteFail": "E2E_TEST_일괄 수주 수동 삭제 필요",
|
||||
"cleanupRequired": "E2E_TEST_일괄 접두사 수주는 테스트 데이터"
|
||||
}
|
||||
}
|
||||
179
settings-calendar-crud.json
Normal file
179
settings-calendar-crud.json
Normal file
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"id": "settings-calendar-crud",
|
||||
"name": "달력 일정 CRUD 테스트",
|
||||
"version": "3.1.0",
|
||||
"enabled": true,
|
||||
"screenshotPolicy": {
|
||||
"captureOnFail": true,
|
||||
"captureOnPass": false
|
||||
},
|
||||
"description": "설정 > 달력관리 메뉴의 달력 일정 등록/조회/수정/삭제 전체 CRUD (Server Actions POST 방식)",
|
||||
"baseUrl": "https://dev.codebridge-x.com",
|
||||
"menuNavigation": {
|
||||
"level1": "설정",
|
||||
"level2": "달력관리",
|
||||
"expectedUrl": "/settings/calendar",
|
||||
"searchWithinParent": true,
|
||||
"closeOtherMenus": true
|
||||
},
|
||||
"auth": { "username": "TestUser5", "password": "password123!" },
|
||||
"testData": {
|
||||
"create": { "name": "E2E_TEST_일정_{timestamp}", "type": "회사일정", "memo": "E2E 자동화 테스트 일정" },
|
||||
"update": { "name": "E2E_TEST_수정일정_{timestamp}" }
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "메뉴 진입: 설정 > 달력관리",
|
||||
"action": "menu_navigate",
|
||||
"level1": "설정",
|
||||
"level2": "달력관리",
|
||||
"expected": { "url_contains": "/settings/calendar" }
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "페이지 로드 대기",
|
||||
"action": "wait",
|
||||
"timeout": 3000
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "목록 탭 전환 (PointerEvent)",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'TAB_SWITCH'};const tab=Array.from(document.querySelectorAll('button[role=\"tab\"],button')).find(b=>b.innerText?.trim()==='목록'&&b.offsetParent!==null);if(tab){const rect=tab.getBoundingClientRect();const x=rect.left+rect.width/2;const y=rect.top+rect.height/2;const opts={bubbles:true,cancelable:true,clientX:x,clientY:y,button:0};tab.dispatchEvent(new PointerEvent('pointerdown',opts));tab.dispatchEvent(new MouseEvent('mousedown',opts));tab.dispatchEvent(new PointerEvent('pointerup',opts));tab.dispatchEvent(new MouseEvent('mouseup',opts));tab.dispatchEvent(new MouseEvent('click',opts));await w(2000);R.switched=true;R.tables=document.querySelectorAll('table').length;}else{R.switched=false;R.info='목록 탭 미발견';}R.ok=true;return JSON.stringify(R);})()"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "테이블 로드 대기",
|
||||
"action": "wait",
|
||||
"timeout": 2000
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "[CREATE] ts 초기화 + 등록 다이얼로그 열기",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const n=new Date();const p=v=>v.toString().padStart(2,'0');const ts=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:'CREATE_OPEN',ts};const btn=Array.from(document.querySelectorAll('button')).find(b=>/달력.*일정.*등록|일정.*등록/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!btn){R.error='달력 일정 등록 버튼 없음';R.ok=false;return JSON.stringify(R);}btn.click();await w(1500);const dlg=document.querySelector('[role=\"dialog\"]');R.dialogOpen=!!dlg;R.ok=!!dlg;if(!dlg)R.error='다이얼로그 미열림';return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "[CREATE] 폼 입력 + 등록 (일정명/유형/날짜/메모)",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CREATE_FORM'};const modal=document.querySelector('[role=\"dialog\"]');if(!modal){R.error='다이얼로그 미열림';R.ok=false;return JSON.stringify(R);}const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'';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 nameInput=modal.querySelector('input[placeholder*=\"일정명\"]');if(nameInput){sv(nameInput,'E2E_TEST_일정_'+ts);await w(300);R.name=true;}else{R.error='일정명 입력 필드 없음';R.ok=false;return JSON.stringify(R);}const combo=modal.querySelector('button[role=\"combobox\"]');if(combo){combo.click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opt=Array.from(lb.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.includes('회사일정'));if(opt){opt.click();await w(400);R.type='회사일정';}else{const opts=lb.querySelectorAll('[role=\"option\"]');if(opts.length>0){opts[opts.length-1].click();await w(400);R.type='last option';}}}else{R.typeWarn='listbox 미표시';}}const pickDay=(idx)=>{const pop=document.querySelector('[data-radix-popper-content-wrapper]');if(!pop)return false;const days=Array.from(pop.querySelectorAll('td button')).filter(b=>b.offsetParent!==null&&!b.disabled&&/^\\d{1,2}$/.test(b.innerText?.trim()));if(days.length>0){days[Math.min(idx,days.length-1)].click();return true;}return false;};const allBtns=Array.from(modal.querySelectorAll('button')).filter(b=>b.offsetParent!==null&&!b.disabled);const startBtn=allBtns.find(b=>b.innerText?.trim()==='시작일'&&!b.getAttribute('role'));if(startBtn){startBtn.click();await w(800);R.startDate=pickDay(14);await w(500);}const endBtn=Array.from(modal.querySelectorAll('button')).filter(b=>b.offsetParent!==null&&!b.disabled).find(b=>b.innerText?.trim()==='종료일'&&!b.getAttribute('role'));if(endBtn){endBtn.click();await w(800);R.endDate=pickDay(21);await w(500);}const memo=modal.querySelector('textarea');if(memo&&memo.offsetParent!==null){const ns2=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value')?.set;if(ns2)ns2.call(memo,'E2E 자동화 테스트');else memo.value='E2E 자동화 테스트';memo.dispatchEvent(new Event('input',{bubbles:true}));memo.dispatchEvent(new Event('change',{bubbles:true}));await w(200);R.memo=true;}const submitBtn=Array.from(modal.querySelectorAll('button')).find(b=>/^등록$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(submitBtn){submitBtn.click();await w(3000);R.submitted=true;const dlgStill=document.querySelector('[role=\"dialog\"]');R.dialogClosed=!dlgStill||dlgStill.offsetParent===null;}else{R.error='등록 버튼 없음';R.ok=false;return JSON.stringify(R);}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 30000
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"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,
|
||||
"name": "[CREATE] 목록에서 등록 결과 확인",
|
||||
"phase": "CREATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'VERIFY_LIST'};const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.rowCount=rows.length;if(rows.length===0){R.info='warn: 테이블 행 없음 (목록 탭 미활성 가능)';R.ok=true;return JSON.stringify(R);}const found=rows.find(r=>r.innerText?.includes('E2E_TEST_일정'));R.found=!!found;if(found)R.rowText=found.innerText?.substring(0,80);R.info=found?'pass: E2E 데이터 목록 확인':'warn: E2E 행 미발견 (rows='+rows.length+')';R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "[UPDATE] E2E 일정 행 클릭 → 수정 다이얼로그",
|
||||
"phase": "UPDATE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const isVis=el=>!!el&&el.getBoundingClientRect().width>0;const R={phase:'EDIT_OPEN'};const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null&&r.innerText?.includes('E2E_TEST_'));if(rows.length>0){rows[0].click();await w(2000);R.clicked=true;R.rowText=rows[0].innerText?.substring(0,60);}else{const allRows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);if(allRows.length>0){allRows[0].click();await w(2000);R.clicked=true;R.info='E2E 행 미발견, 첫 행 사용';}else{R.info='테이블 행 없음';R.ok=true;return JSON.stringify(R);}}const dlg=document.querySelector('[role=\"dialog\"]');R.dialogOpen=isVis(dlg);if(!R.dialogOpen){await w(1500);R.dialogOpen=isVis(document.querySelector('[role=\"dialog\"]'));}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"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;const R={phase:'UPDATE_FORM'};let modal=document.querySelector('[role=\"dialog\"]');if(!isVis(modal)){await w(2000);modal=document.querySelector('[role=\"dialog\"]');if(!isVis(modal)){const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null&&r.innerText?.includes('E2E_TEST_'));if(rows.length>0){rows[0].click();await w(2000);modal=document.querySelector('[role=\"dialog\"]');}if(!isVis(modal)){R.error='다이얼로그 미열림 (재시도 포함)';R.ok=false;return JSON.stringify(R);}}}const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'';const nameInput=modal.querySelector('input[placeholder*=\"일정명\"]')||modal.querySelector('input[type=\"text\"]');if(nameInput&&!nameInput.readOnly){const ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(ns)ns.call(nameInput,'E2E_TEST_수정일정_'+ts);else nameInput.value='E2E_TEST_수정일정_'+ts;nameInput.dispatchEvent(new Event('input',{bubbles:true}));nameInput.dispatchEvent(new Event('change',{bubbles:true}));await w(300);R.nameUpdated=true;}else{R.nameWarn='입력필드 없거나 readOnly';}const allBtns=Array.from(modal.querySelectorAll('button')).filter(b=>!b.disabled);R.btnTexts=allBtns.map(b=>b.innerText?.trim()).filter(t=>t&&t.length<10);const saveBtn=allBtns.find(b=>/^수정$/.test(b.innerText?.trim()));if(saveBtn){saveBtn.click();await w(3000);R.saved=true;}else{const altBtn=allBtns.find(b=>/저장|확인/.test(b.innerText?.trim())&&!/취소|닫기|삭제/.test(b.innerText?.trim()));if(altBtn){altBtn.click();await w(3000);R.saved=true;R.info='대체 버튼: '+altBtn.innerText?.trim();}else{R.error='수정/저장 버튼 없음';R.ok=false;return JSON.stringify(R);}}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 20000
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "[UPDATE] 수정 토스트 확인",
|
||||
"phase": "UPDATE",
|
||||
"action": "verify_toast",
|
||||
"verify": { "contains": "수정|완료|성공" }
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"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": 15,
|
||||
"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\"]');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": 16,
|
||||
"name": "[DELETE] E2E 수정일정 행 클릭 → 삭제",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const isVis=el=>!!el&&el.getBoundingClientRect().width>0;await w(1000);const R={phase:'DELETE_FLOW'};const modal=document.querySelector('[role=\"dialog\"]');if(isVis(modal)){R.info='모달 이미 열림';const delBtn=Array.from(modal.querySelectorAll('button')).find(b=>/^삭제$/.test(b.innerText?.trim())&&!b.disabled);if(delBtn){delBtn.click();await w(1500);const cfm=document.querySelector('[role=\"alertdialog\"]');if(cfm){const cfmBtn=Array.from(cfm.querySelectorAll('button')).find(b=>/삭제|확인|예/.test(b.innerText?.trim()));if(cfmBtn){cfmBtn.click();await w(2000);R.deleted=true;}}else{R.deleted=true;}R.ok=true;return JSON.stringify(R);}}const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null&&r.innerText?.includes('E2E_TEST_'));if(rows.length>0){rows[0].click();await w(2000);R.rowClicked=true;const modal2=document.querySelector('[role=\"dialog\"]');if(isVis(modal2)){const delBtn=Array.from(modal2.querySelectorAll('button')).find(b=>/^삭제$/.test(b.innerText?.trim())&&!b.disabled);if(delBtn){delBtn.click();await w(1500);const cfm=document.querySelector('[role=\"alertdialog\"]');if(cfm){const cfmBtn=Array.from(cfm.querySelectorAll('button')).find(b=>/삭제|확인|예/.test(b.innerText?.trim()));if(cfmBtn){cfmBtn.click();await w(2000);R.deleted=true;}}else{R.deleted=true;R.info='alertdialog 없이 직접 삭제';}}else{R.info='삭제 버튼 없음';}}}else{R.info='E2E 행 없음';}R.ok=true;return JSON.stringify(R);})()",
|
||||
"timeout": 20000,
|
||||
"critical": false
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "[DELETE] 삭제 토스트 확인",
|
||||
"phase": "DELETE",
|
||||
"action": "verify_toast",
|
||||
"verify": { "contains": "삭제|완료|성공" }
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"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": 19,
|
||||
"name": "[DELETE] 목록에서 삭제 확인",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const R={phase:'VERIFY_DELETED'};const body=document.body.innerText;const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'IMPOSSIBLE';R.stillExists=body.includes('E2E_TEST_수정일정_'+ts)||body.includes('E2E_TEST_일정_'+ts);R.ok=!R.stillExists;R.info=R.stillExists?'warn: E2E data still visible':'pass: E2E data removed';return JSON.stringify(R);})()",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"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});}})()"
|
||||
}
|
||||
],
|
||||
"expectedAPIs": [
|
||||
{ "method": "POST", "endpoint": "/settings/calendar-management", "description": "달력 일정 CRUD (Server Action)" }
|
||||
],
|
||||
"rollbackPlan": {
|
||||
"onCreateFail": "다이얼로그 닫기",
|
||||
"onDeleteFail": "E2E_TEST_ 접두사 일정 수동 삭제 필요",
|
||||
"cleanupRequired": "E2E_TEST_ 접두사 일정은 테스트 데이터"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user