Files
sam-scenarios/settings-calendar-crud.json
김보곤 4b5bf7a873 fix: 급여 장기요양 셀렉터 수정(label→span) + 달력 토스트 스텝 제거
- hr-salary-long-term-care: steps 8,9,13,14 셀렉터를 label→span으로 변경, container를 [class*=field]→[class*=flex]로 변경 (실제 DOM 구조 반영)
- settings-calendar-crud: toast verify steps 3개 제거 (Server Actions는 토스트 미사용), 19→16 스텝
2026-03-05 21:51:41 +09:00

151 lines
16 KiB
JSON
Raw Permalink 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": "settings-calendar-crud",
"name": "달력 일정 CRUD 테스트",
"version": "3.3.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] 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": 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": 9,
"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": 10,
"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": 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": 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": 13,
"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": 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": 15,
"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": 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});}})()"
}
],
"expectedAPIs": [
{ "method": "POST", "endpoint": "/settings/calendar-management", "description": "달력 일정 CRUD (Server Action)" }
],
"rollbackPlan": {
"onCreateFail": "다이얼로그 닫기",
"onDeleteFail": "E2E_TEST_ 접두사 일정 수동 삭제 필요",
"cleanupRequired": "E2E_TEST_ 접두사 일정은 테스트 데이터"
}
}