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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "settings-calendar-crud",
|
||||
"name": "달력 일정 CRUD 테스트",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"enabled": true,
|
||||
"screenshotPolicy": {
|
||||
"captureOnFail": true,
|
||||
@@ -95,66 +95,60 @@
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "[UPDATE] E2E 일정 행 클릭 → 수정 다이얼로그",
|
||||
"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
|
||||
"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] 일정명 수정 + 수정 버튼 클릭",
|
||||
"name": "[UPDATE] 수정 토스트 확인 (Server Action: 토스트 없을 수 있음)",
|
||||
"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
|
||||
"action": "verify_toast",
|
||||
"verify": { "contains": "수정|완료|성공" },
|
||||
"critical": false
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"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\"]');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'});})()"
|
||||
"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": 16,
|
||||
"name": "[DELETE] E2E 수정일정 행 클릭 → 삭제",
|
||||
"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;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);})()",
|
||||
"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": 17,
|
||||
"name": "[DELETE] 삭제 토스트 확인",
|
||||
"id": 16,
|
||||
"name": "[DELETE] 삭제 토스트 확인 (Server Action: 토스트 없을 수 있음)",
|
||||
"phase": "DELETE",
|
||||
"action": "verify_toast",
|
||||
"verify": { "contains": "삭제|완료|성공" }
|
||||
"verify": { "contains": "삭제|완료|성공" },
|
||||
"critical": false
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"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": 19,
|
||||
"id": 18,
|
||||
"name": "[DELETE] 목록에서 삭제 확인",
|
||||
"phase": "DELETE",
|
||||
"action": "evaluate",
|
||||
@@ -162,7 +156,7 @@
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"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});}})()"
|
||||
|
||||
Reference in New Issue
Block a user