Files
sam-scenarios/settings-calendar-crud.json
김보곤 b95f7fc132 fix: settings-calendar-crud, hr-salary-long-term-care 안정성 개선
settings-calendar-crud v3.2.0:
- UPDATE steps 11+12 통합: 행 클릭→다이얼로그 열기→수정→저장을 단일 스텝으로 병합
  (스텝 간 다이얼로그 소실 방지, 3회 재시도 + waitDlg 폴링)
- DELETE 스텝: waitDlg 폴링 방식으로 다이얼로그 감지 강화
- 다이얼로그 셀렉터 확장: data-state=open, Sheet, DialogContent 추가
- toast 검증 스텝에 critical:false 추가 (Server Action 토스트 미표시 대응)
- 전체 스텝 20→19로 축소

hr-salary-long-term-care v1.1.0:
- Step 12: 고정 2000ms 대기 → 8초 폴링 방식 (300ms 간격)으로 변경
  (step-executor 3초 기본 타임아웃 충돌 해결)
- Step 12 timeout: 5000→10000ms
- Step 15: offsetParent→getBoundingClientRect 가시성 검사, plain string→JSON 반환
- Step 16: plain string→JSON 반환 (json_fail 경고 해결)
- 다이얼로그 셀렉터 확장: data-state=open Sheet 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:46:14 +09:00

174 lines
16 KiB
JSON
Raw 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.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_ 접두사 일정은 테스트 데이터"
}
}