Files
sam-scenarios/sales-order.json
김보곤 f42cf4ab7d fix: deprecated window.__API_LOGS__ → window.__E2E__.getApiLogs() 패턴 수정 (17개 파일)
- approval-box, edge-rapid-click-acc-sales, full-crud-* (4개)
- hr-salary-long-term-care, production-work-order
- quality-inspection, quality-performance-report
- reload-persist-acc-deposit, sales-management
- sales-order-bulk-delete, sales-order, sales-quotation
- system-dashboard, vendor-management
- 전체 209/209 ALL PASS 검증 완료

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:42:23 +09:00

317 lines
28 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.

{
"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_ 접두사 수주는 테스트 데이터"
}
}