- 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>
180 lines
17 KiB
JSON
180 lines
17 KiB
JSON
{
|
||
"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_ 접두사 일정은 테스트 데이터"
|
||
}
|
||
}
|