Files
sam-scenarios/settings-calendar-crud.json

174 lines
16 KiB
JSON
Raw Normal View History

{
"id": "settings-calendar-crud",
"name": "달력 일정 CRUD 테스트",
"version": "3.2.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 waitDlg=async(ms)=>{const end=Date.now()+ms;while(Date.now()<end){const d=document.querySelector('[role=\"dialog\"],[data-state=\"open\"][class*=\"Sheet\"],[class*=\"DialogContent\"]');if(isVis(d))return d;await w(200);}return null;};const R={phase:'UPDATE_MERGED'};const clickRow=async()=>{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();return rows[0].innerText?.substring(0,60);}const allRows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);if(allRows.length>0){allRows[0].click();return 'fallback: first row';}return null;};let modal=null;for(let attempt=0;attempt<3&&!modal;attempt++){const txt=await clickRow();R.rowText=txt;if(!txt){R.info='테이블 행 없음';R.ok=true;return JSON.stringify(R);}modal=await waitDlg(5000);if(!modal){R.retries=(R.retries||0)+1;await w(1000);}}if(!modal){R.error='다이얼로그 미열림 (3회 재시도)';R.ok=false;return JSON.stringify(R);}R.dialogOpen=true;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": 25000
},
{
"id": 12,
"name": "[UPDATE] 수정 토스트 확인 (Server Action: 토스트 없을 수 있음)",
"phase": "UPDATE",
"action": "verify_toast",
"verify": { "contains": "수정|완료|성공" },
"critical": false
},
{
"id": 13,
"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,
"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,
"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;const waitDlg=async(ms)=>{const end=Date.now()+ms;while(Date.now()<end){const d=document.querySelector('[role=\"dialog\"],[data-state=\"open\"][class*=\"Sheet\"],[class*=\"DialogContent\"]');if(isVis(d))return d;await w(200);}return null;};await w(1000);const R={phase:'DELETE_FLOW'};let modal=await waitDlg(1000);if(!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();modal=await waitDlg(5000);R.rowClicked=true;}else{R.info='E2E 행 없음';R.ok=true;return JSON.stringify(R);}}if(!modal){R.info='다이얼로그 미열림';R.ok=true;return JSON.stringify(R);}R.dialogOpen=true;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.info='alertdialog 없이 직접 삭제';}}else{R.info='삭제 버튼 없음';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 20000,
"critical": false
},
{
"id": 16,
"name": "[DELETE] 삭제 토스트 확인 (Server Action: 토스트 없을 수 있음)",
"phase": "DELETE",
"action": "verify_toast",
"verify": { "contains": "삭제|완료|성공" },
"critical": false
},
{
"id": 17,
"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,
"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": 19,
"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_ 접두사 일정은 테스트 데이터"
}
}