Files
sam-scenarios/sales-order.json

317 lines
28 KiB
JSON
Raw Permalink Normal View History

{
"enabled": true,
"id": "sales-order",
"name": "수주관리 CRUD + 계산검증 테스트",
"version": "2.0.0",
"screenshotPolicy": {
"captureOnFail": true,
"captureOnPass": false
},
"description": "판매관리 > 수주관리 메뉴의 수주 조회/등록/수정/삭제 전체 CRUD + 품목 입력 + 자동계산 검증 + API 검증",
"baseUrl": "https://dev.codebridge-x.com",
"menuNavigation": {
"level1": "판매관리",
"level2": "수주관리",
"expectedUrl": "/sales/order-management-sales",
"searchWithinParent": true,
"closeOtherMenus": true
},
"auth": {
"username": "TestUser5",
"password": "password123!"
},
"testData": {
"create": {
"siteName": "E2E_TEST_현장_{timestamp}",
"manager": "E2E 담당자",
"phone": "010-1234-5678",
"receiver": "E2E 수신자",
"receiverAddr": "E2E_TEST_수신처",
"itemName": "E2E_TEST_품목",
"quantity": "10",
"unitPrice": "50000",
"expectedSupply": "500,000",
"expectedVat": "50,000",
"expectedTotal": "550,000"
},
"update": {
"quantity": "20",
"expectedSupply": "1,000,000",
"expectedVat": "100,000",
"expectedTotal": "1,100,000"
}
},
"steps": [
{
"id": 1,
"name": "메뉴 진입: 판매관리 > 수주관리",
"action": "menu_navigate",
"level1": "판매관리",
"level2": "수주관리",
"expected": {
"url_contains": "/sales/order",
"visible": ["수주관리", "수주"]
}
},
{
"id": 2,
"name": "페이지 로드 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 3,
"name": "URL 검증",
"action": "verify_url",
"expected": {
"url_contains": "/sales/order-management-sales"
}
},
{
"id": 4,
"name": "필수 검증 #5: 목업 페이지 감지",
"action": "verify_not_mockup",
"checks": ["수주 목록 표시", "수주 등록 버튼 존재", "검색/필터 기능 존재"],
"expected": "정상 페이지 (목업 아님)"
},
{
"id": 5,
"name": "수주 테이블 로드 대기",
"action": "wait_for_table",
"timeout": 15000
},
{
"id": 6,
"name": "[CREATE] ts 초기화 + 등록 버튼 클릭",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));try{sessionStorage.removeItem('__E2E_TS__');}catch(e){}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(2500);R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 7,
"name": "[CREATE] 등록 폼 로드 대기",
"phase": "CREATE",
"action": "wait",
"timeout": 2000
},
{
"id": 8,
"name": "[CREATE] 수주처(거래처) 콤보박스 선택",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'COMBO_CLIENT'};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],[class*=Sidebar]'));R.comboCount=combos.length;const clientCombo=combos.find(b=>{const lbl=b.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"],label')?.innerText||'';return lbl.includes('수주처')||lbl.includes('거래처')||lbl.includes('고객');});const target=clientCombo||combos[0];if(!target){R.info='수주처 combobox 미발견';R.ok=true;return JSON.stringify(R);}document.body.click();await w(100);target.scrollIntoView({block:'center'});target.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=opts[0].innerText?.trim().substring(0,30);R.ok=true;}else{R.info='옵션 없음';R.ok=true;}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);R.info='listbox 미열림';R.ok=true;}return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 9,
"name": "[CREATE] 배송방식 콤보박스 선택",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'COMBO_DELIVERY'};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],[class*=Sidebar]'));const deliveryCombo=combos.find(b=>{const lbl=b.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"],label')?.innerText||'';return lbl.includes('배송')||lbl.includes('운송')||lbl.includes('delivery');});if(!deliveryCombo){R.info='배송방식 combobox 미발견';R.ok=true;return JSON.stringify(R);}document.body.click();await w(100);deliveryCombo.scrollIntoView({block:'center'});deliveryCombo.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=opts[0].innerText?.trim().substring(0,30);}else{R.info='옵션 없음';}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);R.info='listbox 미열림';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 10,
"name": "[CREATE] 운임비용 콤보박스 선택",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'COMBO_FREIGHT'};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],[class*=Sidebar]'));const freightCombo=combos.find(b=>{const lbl=b.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"],label')?.innerText||'';return lbl.includes('운임')||lbl.includes('freight')||lbl.includes('비용');});if(!freightCombo){R.info='운임비용 combobox 미발견';R.ok=true;return JSON.stringify(R);}document.body.click();await w(100);freightCombo.scrollIntoView({block:'center'});freightCombo.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=opts[0].innerText?.trim().substring(0,30);}else{R.info='옵션 없음';}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);R.info='listbox 미열림';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 11,
"name": "[CREATE] 기본정보 텍스트 필드 입력",
"phase": "CREATE",
"action": "fill_form",
"fields": [
{ "name": "현장명", "value": "E2E_TEST_현장_{timestamp}" },
{ "name": "담당자", "value": "E2E 담당자" },
{ "name": "연락처", "value": "010-1234-5678" },
{ "name": "수신자", "value": "E2E 수신자" },
{ "name": "수신처", "value": "E2E_TEST_수신처" }
]
},
{
"id": 12,
"name": "[CREATE] 납기일 날짜 선택",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DATE_PICK'};const formArea=document.querySelector('main,[class*=\"content\"]')||document.body;const dateBtn=Array.from(formArea.querySelectorAll('button')).find(b=>{const lbl=b.closest('[class*=\"field\"],[class*=\"Field\"],[class*=\"form-item\"],[class*=\"row\"],label')?.innerText||'';const txt=b.innerText?.trim()||'';return(lbl.includes('납기')||lbl.includes('delivery')||lbl.includes('기한'))&&b.offsetParent!==null;})||Array.from(formArea.querySelectorAll('button')).find(b=>b.innerText?.trim()==='날짜 선택'&&b.offsetParent!==null);if(!dateBtn){R.info='납기일 날짜버튼 미발견';R.ok=true;return JSON.stringify(R);}dateBtn.scrollIntoView({block:'center'});await w(100);dateBtn.click();await w(600);if(!document.querySelector('table[class*=\"rdp\"],.rdp-month,[role=\"grid\"]')){dateBtn.click();await w(600);}const today=document.querySelector('[aria-selected=\"true\"]')||document.querySelector('button[name=\"day\"].bg-primary')||Array.from(document.querySelectorAll('button[name=\"day\"],td[role=\"gridcell\"] button,.rdp-day button')).find(b=>b.getAttribute('aria-selected')==='true'||b.classList.contains('bg-primary')||b.tabIndex===0)||document.querySelector('button[name=\"day\"]')||document.querySelector('td[role=\"gridcell\"] button');if(today){today.click();await w(300);R.dateSelected=true;}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);R.dateSelected=false;}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 13,
"name": "[CREATE] 품목 추가 버튼 클릭",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'ADD_ITEM'};const addBtn=Array.from(document.querySelectorAll('button')).find(b=>{const t=b.innerText?.trim()||'';return(t==='+'||t.includes('추가')||t.includes('품목 추가')||t.includes('행 추가'))&&b.offsetParent!==null&&!b.disabled;});if(addBtn){addBtn.click();await w(1000);R.clicked=true;}else{R.info='품목 추가 버튼 미발견 (이미 입력행 존재 가능)';R.clicked=false;}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 14,
"name": "[CREATE] 품목 입력: 수량=10, 단가=50,000",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const sv=(el,v)=>{const p=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;const ns=Object.getOwnPropertyDescriptor(p,'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 ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'';const R={phase:'ITEM_INPUT'};const formArea=document.querySelector('main,[class*=\"content\"]')||document.body;const inputs=Array.from(formArea.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);let filled=0;const itemInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||'';return ph.includes('품목')||nm.includes('item')||lbl.includes('품목')||ph.includes('제품');});if(itemInput){sv(itemInput,'E2E_TEST_품목_'+ts);filled++;await w(200);}const qtyInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||'';return ph.includes('수량')||nm.includes('quantity')||nm.includes('qty')||lbl.includes('수량');});if(qtyInput){sv(qtyInput,'10');filled++;await w(200);}const priceInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||'';return ph.includes('단가')||nm.includes('price')||nm.includes('unitPrice')||lbl.includes('단가');});if(priceInput){sv(priceInput,'50000');filled++;await w(300);}const noteInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';return ph.includes('적요')||nm.includes('note')||ph.includes('비고');});if(noteInput){sv(noteInput,'E2E_TEST_적요_'+ts);filled++;await w(200);}await w(500);R.filled=filled;R.ok=true;R.info=filled>0?'pass: filled '+filled+' item fields':'warn: no item fields found (form structure may differ)';return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 15,
"name": "[CREATE] 금액 자동계산 검증: 10×50,000=500,000 / VAT 50,000 / 합계 550,000",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(800);const R={phase:'CALC_VERIFY'};const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null);const allVals=[pageText,...inputs.map(i=>i.value||'')].join(' ');R.hasSupply500000=allVals.includes('500,000')||allVals.includes('500000');R.hasVat50000=allVals.includes('50,000')||allVals.includes('50000');R.hasTotal550000=allVals.includes('550,000')||allVals.includes('550000');R.ok=true;R.info=[R.hasSupply500000?'pass: supply=500,000':'warn: supply 500,000 not found',R.hasVat50000?'pass: vat=50,000':'warn: vat 50,000 not found',R.hasTotal550000?'pass: total=550,000':'warn: total 550,000 not found'].join(' | ');return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 16,
"name": "[CREATE] 등록 저장 클릭",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SUBMIT'};const sub=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!sub){R.error='등록/저장 버튼 없음';R.ok=false;return JSON.stringify(R);}sub.click();await w(3000);const t=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');R.toast={count:t.length,text:t.length>0?Array.from(t).pop()?.innerText?.trim().substring(0,100):''};R.ok=true;return JSON.stringify(R);})()",
"timeout": 20000
},
{
"id": 17,
"name": "[CREATE] 저장 완료 토스트 확인",
"phase": "CREATE",
"action": "verify_toast",
"verify": { "contains": "등록|완료|성공|저장" }
},
{
"id": 18,
"name": "[CREATE] API POST 검증",
"phase": "CREATE",
"action": "evaluate",
"script": "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().logs:[]);const posts=logs.filter(l=>l.method==='POST'&&l.status>=200&&l.status<300);return posts.length>0?'pass: POST '+posts[posts.length-1].status+' ('+posts[posts.length-1].url.split('/').pop()+')':'warn: no successful POST found';})()"
},
{
"id": 19,
"name": "[CREATE] 등록 후 목록 복귀",
"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 JSON.stringify({url:location.pathname+location.search});})()",
"timeout": 15000
},
{
"id": 20,
"name": "[CREATE] 목록 안정화 대기",
"phase": "CREATE",
"action": "wait",
"timeout": 2000
},
{
"id": 21,
"name": "[CREATE] 등록 결과 확인 (목록에서 상태 포함)",
"phase": "CREATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'';const R={phase:'VERIFY_LIST'};await w(500);const rows=Array.from(document.querySelectorAll('table tbody tr'));R.rowCount=rows.length;let found=rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(!found){const ths=document.querySelectorAll('table thead th');const sortTh=Array.from(ths).find(th=>/일자|날짜|No|번호/.test(th.innerText?.trim()));if(sortTh){sortTh.click();await w(1000);sortTh.click();await w(1000);const rows2=Array.from(document.querySelectorAll('table tbody tr'));found=rows2.find(r=>r.innerText?.includes('E2E_TEST_'));}}if(found){R.found=true;const txt=found.innerText||'';R.hasStatus=txt.includes('대기')||txt.includes('등록')||txt.includes('진행')||txt.includes('미확인');R.statusInfo=R.hasStatus?'pass: initial status found':'warn: status column not detected';R.rowText=txt.substring(0,120);}else{R.found=false;R.statusInfo='warn: E2E row not found';}R.ok=R.found||R.rowCount>0;return JSON.stringify(R);})()",
"timeout": 20000
},
{
"id": 22,
"name": "[READ] 상세 페이지 진입",
"phase": "READ",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'READ_ENTER'};const rows=Array.from(document.querySelectorAll('table tbody tr'));const testRow=rows.find(r=>r.innerText?.includes('E2E_TEST_'));const targetRow=testRow||rows[0];if(!targetRow){R.error='행 없음';R.ok=false;return JSON.stringify(R);}R.usedTestRow=!!testRow;targetRow.click();await w(3000);R.detailUrl=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 23,
"name": "[READ] 상세 페이지 로드 대기",
"phase": "READ",
"action": "wait",
"timeout": 2000
},
{
"id": 24,
"name": "[READ] 상세 필드 검증 (현장명, 수량, 단가, 금액)",
"phase": "READ",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DETAIL_VERIFY'};const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input,textarea,select')).filter(i=>i.offsetParent!==null);const allValues=[pageText,...inputs.map(i=>i.value||'')].join(' ');const checks={'E2E_TEST_':allValues.includes('E2E_TEST_'),'수량_10':allValues.includes('10'),'단가_50000':allValues.includes('50,000')||allValues.includes('50000'),'공급가액_500000':allValues.includes('500,000')||allValues.includes('500000')};R.checks=checks;const matched=Object.values(checks).filter(Boolean).length;R.matched=matched;R.total=Object.keys(checks).length;R.ok=matched>=2;R.info='pass: '+matched+'/'+R.total+' fields matched in detail';return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 25,
"name": "[UPDATE] 수정 모드 진입",
"phase": "UPDATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'EDIT_MODE'};const editBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정'&&b.offsetParent!==null&&!b.disabled);R.editBtnExists=!!editBtn;if(editBtn){editBtn.click();await w(2000);R.editUrl=location.pathname+location.search;R.isEditMode=location.search.includes('mode=edit');const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);R.editableFields=inputs.length;R.ok=R.editableFields>0||R.isEditMode;}else{R.ok=true;R.info='수정 버튼 없음';}return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 26,
"name": "[UPDATE] 수량 변경: 10 → 20",
"phase": "UPDATE",
"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:'UPDATE_QTY'};const formArea=document.querySelector('main,[class*=\"content\"]')||document.body;const inputs=Array.from(formArea.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const qtyInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||'';return ph.includes('수량')||nm.includes('quantity')||nm.includes('qty')||lbl.includes('수량');});if(qtyInput){sv(qtyInput,'20');await w(500);R.updated=true;R.newValue=qtyInput.value;}else{R.info='수량 필드 미발견';R.updated=false;}R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 27,
"name": "[UPDATE] 재계산 검증: 20×50,000=1,000,000 / VAT 100,000 / 합계 1,100,000",
"phase": "UPDATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(800);const R={phase:'RECALC_VERIFY'};const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input')).filter(i=>i.offsetParent!==null);const allVals=[pageText,...inputs.map(i=>i.value||'')].join(' ');R.hasSupply1000000=allVals.includes('1,000,000')||allVals.includes('1000000');R.hasVat100000=allVals.includes('100,000')||allVals.includes('100000');R.hasTotal1100000=allVals.includes('1,100,000')||allVals.includes('1100000');R.ok=true;R.info=[R.hasSupply1000000?'pass: supply=1,000,000':'warn: supply 1,000,000 not found',R.hasVat100000?'pass: vat=100,000':'warn: vat 100,000 not found',R.hasTotal1100000?'pass: total=1,100,000':'warn: total 1,100,000 not found'].join(' | ');return JSON.stringify(R);})()",
"timeout": 10000
},
{
"id": 28,
"name": "[UPDATE] 수정 저장 클릭",
"phase": "UPDATE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'UPDATE_SAVE'};const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>/^저장$|^수정$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!saveBtn){R.error='저장 버튼 없음';R.ok=false;return JSON.stringify(R);}saveBtn.click();await w(3000);const t=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');R.toast={count:t.length,text:t.length>0?Array.from(t).pop()?.innerText?.trim().substring(0,100):''};R.ok=true;return JSON.stringify(R);})()",
"timeout": 20000
},
{
"id": 29,
"name": "[UPDATE] API PUT 검증",
"phase": "UPDATE",
"action": "evaluate",
"script": "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().logs:[]);const puts=logs.filter(l=>(l.method==='PUT'||l.method==='PATCH')&&l.status>=200&&l.status<300);return puts.length>0?'pass: '+puts[puts.length-1].method+' '+puts[puts.length-1].status+' ('+puts[puts.length-1].url.split('/').pop()+')':'warn: no successful PUT/PATCH found';})()"
},
{
"id": 30,
"name": "[DELETE] 삭제 처리",
"phase": "DELETE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DELETE'};let delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제'&&b.offsetParent!==null);if(!delBtn){const cancelBtn=Array.from(document.querySelectorAll('button,a')).find(b=>/목록|취소|뒤로/.test(b.innerText?.trim()));if(cancelBtn){cancelBtn.click();await w(2000);}else{history.back();await w(2000);}await w(1000);const rows=Array.from(document.querySelectorAll('table tbody tr'));const testRow=rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(testRow){testRow.click();await w(3000);delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제'&&b.offsetParent!==null);}}if(!delBtn){R.info='삭제 버튼 없음 - 스킵';R.ok=true;return JSON.stringify(R);}delBtn.click();await w(1500);let cfm=document.querySelector('[role=\"alertdialog\"] [data-slot=\"alert-dialog-footer\"] button:last-child');if(!cfm){cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button')).find(b=>/삭제/.test(b.innerText?.trim())&&b!==delBtn);}if(!cfm){cfm=Array.from(document.querySelectorAll('button')).find(b=>/확인|삭제|예/.test(b.innerText?.trim())&&b!==delBtn&&b.offsetParent!==null);}if(cfm){cfm.click();await w(3000);}R.ok=true;return JSON.stringify(R);})()",
"timeout": 30000,
"critical": false
},
{
"id": 31,
"name": "[DELETE] API DELETE 검증 + 목록 복귀",
"phase": "DELETE",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DELETE_VERIFY'};const logs=(window.__E2E__?window.__E2E__.getApiLogs().logs:[]);const dels=logs.filter(l=>l.method==='DELETE'&&l.status>=200&&l.status<300);R.deleteApiFound=dels.length>0;if(dels.length>0)R.deleteStatus=dels[dels.length-1].status;await w(2000);const onForm=location.search.includes('mode=view')||location.search.includes('mode=edit')||new RegExp('/(new|[0-9]+|[0-9a-f]{8,})$').test(location.pathname);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);}}await w(1000);const rows=Array.from(document.querySelectorAll('table tbody tr'));const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'IMPOSSIBLE';const stillExists=rows.some(r=>r.innerText?.includes(ts)&&r.innerText?.includes('E2E_TEST_'));R.stillExists=stillExists;R.ok=!stillExists;R.info=stillExists?'warn: E2E data still in list':'pass: E2E data removed from list';return JSON.stringify(R);})()",
"timeout": 20000
},
{
"id": 32,
"name": "[SUMMARY] API 호출 통계",
"action": "evaluate",
"script": "(()=>{const logs=(window.__E2E__?window.__E2E__.getApiLogs().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'||l.method==='PATCH').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,slowCalls:logs.filter(l=>l.duration>2000).length};return 'pass: API summary - total='+summary.total+' GET='+summary.GET+' POST='+summary.POST+' PUT='+summary.PUT+' DELETE='+summary.DELETE+' success='+summary.success+' failed='+summary.failed+' avg='+summary.avgResponseTime+'ms slow='+summary.slowCalls;})()"
}
],
"expectedAPIs": [
{ "method": "GET", "endpoint": "/api/v1/sales-orders", "description": "수주 목록 조회" },
{ "method": "POST", "endpoint": "/api/v1/sales-orders", "description": "수주 등록" },
{ "method": "GET", "endpoint": "/api/v1/sales-orders/{id}", "description": "수주 상세 조회" },
{ "method": "PUT", "endpoint": "/api/v1/sales-orders/{id}", "description": "수주 수정" },
{ "method": "DELETE", "endpoint": "/api/v1/sales-orders/{id}", "description": "수주 삭제" }
],
"requiredVerifications": [
{ "id": 2, "name": "등록/저장 버튼", "steps": [16, 28], "criteria": "API 호출 + 성공 토스트 + 데이터 반영" },
{ "id": 3, "name": "계산 검증", "steps": [15, 27], "criteria": "수량×단가=공급가액, VAT 10%, 합계" },
{ "id": 5, "name": "목업 페이지 감지", "steps": [4], "criteria": "수주 목록, 등록 버튼, 필터 존재" },
{ "id": 6, "name": "삭제 기능", "steps": [30, 31], "criteria": "DELETE API + 목록에서 제거" }
],
"rollbackPlan": {
"onCreateFail": "모달 닫기",
"onUpdateFail": "테스트 수주 수동 삭제 필요",
"onDeleteFail": "테스트 수주 수동 삭제 필요",
"cleanupRequired": "E2E_TEST_ 접두사 수주는 테스트 데이터"
}
}