Files
sam-scenarios/sales-order-bulk-delete.json
김보곤 35739c396f feat: 달력 CRUD, 수주 일괄삭제, 급여 장기요양보험 시나리오 추가
- 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>
2026-03-03 20:48:19 +09:00

186 lines
14 KiB
JSON

{
"id": "sales-order-bulk-delete",
"name": "수주 일괄삭제 테스트",
"version": "1.0.0",
"enabled": true,
"screenshotPolicy": {
"captureOnFail": true,
"captureOnPass": false
},
"description": "판매관리 > 수주관리의 수주 일괄삭제(bulk delete) 기능 검증: 체크박스 선택 → 일괄삭제 → 확인 → API DELETE /orders/bulk",
"baseUrl": "https://dev.codebridge-x.com",
"menuNavigation": {
"level1": "판매관리",
"level2": "수주관리",
"expectedUrl": "/sales/order-management-sales",
"searchWithinParent": true,
"closeOtherMenus": true
},
"auth": { "username": "TestUser5", "password": "password123!" },
"testData": {
"create1": { "siteName": "E2E_TEST_일괄1_{timestamp}" },
"create2": { "siteName": "E2E_TEST_일괄2_{timestamp}" }
},
"steps": [
{
"id": 1,
"name": "메뉴 진입: 판매관리 > 수주관리",
"action": "menu_navigate",
"level1": "판매관리",
"level2": "수주관리",
"expected": { "url_contains": "/sales/order" }
},
{
"id": 2,
"name": "페이지 로드 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 3,
"name": "URL 검증",
"action": "verify_url",
"expected": { "url_contains": "/sales/order-management-sales" }
},
{
"id": 4,
"name": "목업 감지",
"action": "verify_not_mockup",
"checks": ["수주 목록 표시", "등록 버튼 존재"]
},
{
"id": 5,
"name": "테이블 로드 대기",
"action": "wait_for_table",
"timeout": 15000
},
{
"id": 6,
"name": "[CREATE-1] 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:'CREATE1_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(2500);R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 7,
"name": "[CREATE-1] 수주처 콤보박스 선택",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'COMBO1'};const formArea=document.querySelector('main,[class*=\"content\"]')||document.body;const combos=Array.from(formArea.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null&&!b.closest('nav,[class*=sidebar]'));if(combos.length>0){combos[0].click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opts=lb.querySelectorAll('[role=\"option\"]');if(opts.length>0){opts[0].click();await w(400);R.selected=true;}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 8,
"name": "[CREATE-1] 현장명 입력",
"phase": "CREATE",
"action": "fill_form",
"fields": [
{ "name": "현장명", "value": "E2E_TEST_일괄1_{timestamp}" }
]
},
{
"id": 9,
"name": "[CREATE-1] 품목 추가 + 최소 입력",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));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 R={phase:'ITEM1'};const addBtn=Array.from(document.querySelectorAll('button')).find(b=>(b.innerText?.trim()==='+'||b.innerText?.includes('추가'))&&b.offsetParent!==null&&!b.disabled);if(addBtn){addBtn.click();await w(1000);}const inputs=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('수량')||i.name?.includes('qty')||i.name?.includes('quantity');});if(qtyInput){sv(qtyInput,'1');await w(200);}const priceInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('단가')||i.name?.includes('price');});if(priceInput){sv(priceInput,'10000');await w(200);}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 10,
"name": "[CREATE-1] 저장",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const btn=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(btn){btn.click();await w(3000);}return 'pass: save clicked';})()",
"timeout": 15000
},
{
"id": 11,
"name": "[CREATE-1] 토스트 확인 + 목록 복귀",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const onForm=location.search.includes('mode=new')||location.search.includes('mode=edit');if(onForm){const btn=Array.from(document.querySelectorAll('button,a')).find(b=>/목록|취소|뒤로/.test(b.innerText?.trim()));if(btn){btn.click();await w(2000);}else{history.back();await w(2000);}}return 'pass: returned to list';})()",
"timeout": 15000
},
{
"id": 12,
"name": "[CREATE-2] 두번째 수주 등록",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'CREATE2'};const btn=Array.from(document.querySelectorAll('button')).find(b=>/수주.*등록|등록/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!btn){R.info='등록 버튼 없음';R.ok=true;return JSON.stringify(R);}btn.click();await w(2500);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 combos=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null&&!b.closest('nav,[class*=sidebar]'));if(combos.length>0){combos[0].click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opts=lb.querySelectorAll('[role=\"option\"]');if(opts.length>0){opts[0].click();await w(400);}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}const ts=window.__E2E_TS__||'';const siteInput=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled).find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||'';return lbl.includes('현장');});if(siteInput)sv(siteInput,'E2E_TEST_일괄2_'+ts);await w(300);const addBtn=Array.from(document.querySelectorAll('button')).find(b=>(b.innerText?.trim()==='+'||b.innerText?.includes('추가'))&&b.offsetParent!==null&&!b.disabled);if(addBtn){addBtn.click();await w(1000);}const inputs=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('수량')||i.name?.includes('qty');});if(qtyInput)sv(qtyInput,'1');await w(200);const priceInput=inputs.find(i=>{const lbl=i.closest('[class*=field],[class*=Field]')?.querySelector('label')?.innerText||i.placeholder||i.name||'';return lbl.includes('단가')||i.name?.includes('price');});if(priceInput)sv(priceInput,'10000');await w(200);const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(saveBtn){saveBtn.click();await w(3000);}R.ok=true;return JSON.stringify(R);})()",
"timeout": 30000
},
{
"id": 13,
"name": "[CREATE-2] 목록 복귀",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const onForm=location.search.includes('mode=new')||location.search.includes('mode=edit');if(onForm){const btn=Array.from(document.querySelectorAll('button,a')).find(b=>/목록|취소|뒤로/.test(b.innerText?.trim()));if(btn){btn.click();await w(2000);}else{history.back();await w(2000);}}return 'pass: returned to list';})()",
"timeout": 15000
},
{
"id": 14,
"name": "[BULK-DELETE] 목록 안정화 대기",
"phase": "DELETE",
"action": "wait",
"timeout": 3000
},
{
"id": 15,
"name": "[BULK-DELETE] 체크박스 UI 확인 + E2E 행 선택",
"phase": "DELETE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHECKBOX_SELECT'};const rows=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);const e2eRows=rows.filter(r=>r.innerText?.includes('E2E_TEST_일괄'));R.e2eRowCount=e2eRows.length;let selected=0;for(const row of e2eRows){const cb=row.querySelector('input[type=\"checkbox\"],button[role=\"checkbox\"],[data-state]');if(cb){cb.click();await w(300);selected++;}}R.selected=selected;if(selected===0&&rows.length>0){const allCb=document.querySelector('thead input[type=\"checkbox\"],thead button[role=\"checkbox\"],th input[type=\"checkbox\"],th button[role=\"checkbox\"]');if(allCb){R.info='E2E 행 미발견, 전체 선택 확인만';R.hasSelectAll=true;}else{R.info='체크박스 UI 미발견';}}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 16,
"name": "[BULK-DELETE] 일괄삭제 버튼 클릭",
"phase": "DELETE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'BULK_DELETE_CLICK'};const delBtn=Array.from(document.querySelectorAll('button')).find(b=>{const t=b.innerText?.trim()||'';return(t.includes('삭제')||t.includes('일괄')||t.includes('완전삭제'))&&b.offsetParent!==null&&!b.disabled;});if(!delBtn){R.info='일괄삭제 버튼 미발견 (선택 필요)';R.ok=true;return JSON.stringify(R);}delBtn.click();await w(1500);R.clicked=true;R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 17,
"name": "[BULK-DELETE] 확인 다이얼로그 처리",
"phase": "DELETE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CONFIRM_DELETE'};const dialog=document.querySelector('[role=\"alertdialog\"],[role=\"dialog\"]');if(dialog&&dialog.offsetParent!==null){const cfmBtn=Array.from(dialog.querySelectorAll('button')).find(b=>/삭제|확인|예/.test(b.innerText?.trim()));if(cfmBtn){cfmBtn.click();await w(3000);R.confirmed=true;}else{R.info='확인 버튼 미발견';}}else{R.info='다이얼로그 미표시';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000,
"critical": false
},
{
"id": 18,
"name": "[BULK-DELETE] API DELETE /orders/bulk 검증",
"phase": "DELETE",
"action": "evaluate",
"script": "(()=>{const logs=window.__API_LOGS__||[];const bulkDels=logs.filter(l=>l.method==='DELETE'&&(l.url.includes('bulk')||l.url.includes('orders'))&&l.status>=200&&l.status<300);const singleDels=logs.filter(l=>l.method==='DELETE'&&l.status>=200&&l.status<300);return bulkDels.length>0?'pass: BULK DELETE '+bulkDels[bulkDels.length-1].status+' url='+bulkDels[bulkDels.length-1].url.split('/').slice(-2).join('/'):singleDels.length>0?'pass: DELETE found ('+singleDels.length+' calls)':'warn: no DELETE API calls found';})()"
},
{
"id": 19,
"name": "[BULK-DELETE] 목록에서 E2E 데이터 제거 확인",
"phase": "DELETE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(2000);const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'IMPOSSIBLE';const R={phase:'VERIFY_DELETED'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const remaining=rows.filter(r=>r.innerText?.includes('E2E_TEST_일괄')&&r.innerText?.includes(ts));R.remaining=remaining.length;R.ok=remaining.length===0;R.info=remaining.length===0?'pass: E2E bulk data removed':'warn: '+remaining.length+' E2E rows still exist';return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 20,
"name": "[SUMMARY] API 호출 통계",
"action": "evaluate",
"script": "(()=>{const logs=window.__API_LOGS__||[];const summary={total:logs.length,GET:logs.filter(l=>l.method==='GET').length,POST:logs.filter(l=>l.method==='POST').length,PUT:logs.filter(l=>l.method==='PUT').length,DELETE:logs.filter(l=>l.method==='DELETE').length,success:logs.filter(l=>l.status>=200&&l.status<300).length,failed:logs.filter(l=>l.status>=400).length,avgResponseTime:logs.length>0?Math.round(logs.reduce((s,l)=>s+(l.duration||0),0)/logs.length):0};return 'pass: API summary - total='+summary.total+' POST='+summary.POST+' DELETE='+summary.DELETE+' success='+summary.success+' failed='+summary.failed+' avg='+summary.avgResponseTime+'ms';})()"
}
],
"expectedAPIs": [
{ "method": "GET", "endpoint": "/api/v1/orders", "description": "수주 목록 조회" },
{ "method": "POST", "endpoint": "/api/v1/orders", "description": "수주 등록 (2건)" },
{ "method": "DELETE", "endpoint": "/api/v1/orders/bulk", "description": "수주 일괄삭제" }
],
"rollbackPlan": {
"onCreateFail": "등록 실패 시 테스트 종료",
"onDeleteFail": "E2E_TEST_일괄 수주 수동 삭제 필요",
"cleanupRequired": "E2E_TEST_일괄 접두사 수주는 테스트 데이터"
}
}