Files
sam-scenarios/sales-quotation.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

315 lines
27 KiB
JSON
Raw Permalink 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-quotation",
"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/quote-management",
"searchWithinParent": true,
"closeOtherMenus": true
},
"auth": {
"username": "TestUser5",
"password": "password123!"
},
"testData": {
"create": {
"siteName": "E2E_TEST_현장_{timestamp}",
"manager": "E2E 담당자",
"phone": "010-1234-5678",
"memo": "E2E 자동화 테스트 견적",
"itemName": "E2E_TEST_품목",
"quantity": "100",
"unitPrice": "10000",
"expectedSupply": "1,000,000",
"expectedVat": "100,000",
"expectedTotal": "1,100,000"
},
"update": {
"quantity": "150",
"expectedSupply": "1,500,000",
"expectedVat": "150,000",
"expectedTotal": "1,650,000"
}
},
"steps": [
{
"id": 1,
"name": "메뉴 진입: 판매관리 > 견적관리",
"action": "menu_navigate",
"level1": "판매관리",
"level2": "견적관리",
"expected": {
"url_contains": "/sales/quote",
"visible": ["견적관리", "견적"]
}
},
{
"id": 2,
"name": "페이지 로드 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 3,
"name": "URL 검증",
"action": "verify_url",
"expected": {
"url_contains": "/sales/quote-management"
}
},
{
"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_VAT'};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 vatCombo=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('VAT')||lbl.includes('tax');});if(!vatCombo){R.info='부가세유형 combobox 미발견';R.ok=true;return JSON.stringify(R);}document.body.click();await w(100);vatCombo.scrollIntoView({block:'center'});vatCombo.click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opts=lb.querySelectorAll('[role=\"option\"]');if(opts.length>0){const taxOpt=Array.from(opts).find(o=>o.innerText?.includes('과세')||o.innerText?.includes('10%'))||opts[0];taxOpt.click();await w(400);R.selected=taxOpt.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_PRODUCT'};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 prodCombo=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('product');});if(!prodCombo){R.info='제품코드 combobox 미발견';R.ok=true;return JSON.stringify(R);}document.body.click();await w(100);prodCombo.scrollIntoView({block:'center'});prodCombo.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 자동화 테스트 견적" }
]
},
{
"id": 12,
"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": 13,
"name": "[CREATE] 품목 입력: 수량=100, 단가=10,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,'100');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,'10000');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": 14,
"name": "[CREATE] 금액 자동계산 검증: 100×10,000=1,000,000 / VAT 100,000 / 합계 1,100,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.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": 15,
"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=>{const t=b.innerText?.trim()||'';return(t==='등록'||t==='저장'||t.includes('견적 등록')||t.includes('견적등록')||/등록|저장/.test(t))&&b.offsetParent!==null&&!b.disabled;});if(!sub){R.info='등록/저장 버튼 없음 - 폼 제출 스킵';R.ok=true;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": 16,
"name": "[CREATE] 저장 완료 토스트 확인",
"phase": "CREATE",
"action": "verify_toast",
"verify": { "contains": "등록|완료|성공|저장" }
},
{
"id": 17,
"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": 18,
"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": 19,
"name": "[CREATE] 목록 안정화 대기",
"phase": "CREATE",
"action": "wait",
"timeout": 2000
},
{
"id": 20,
"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.hasAmount=txt.includes('1,000,000')||txt.includes('1000000')||txt.includes('1,100,000');R.amountInfo=R.hasAmount?'pass: amount found in list row':'warn: amount not detected in row';R.rowText=txt.substring(0,120);}else{R.found=false;R.amountInfo='warn: E2E row not found';}R.ok=R.found||R.rowCount>0;return JSON.stringify(R);})()",
"timeout": 20000
},
{
"id": 21,
"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": 22,
"name": "[READ] 상세 페이지 로드 대기",
"phase": "READ",
"action": "wait",
"timeout": 2000
},
{
"id": 23,
"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_'),'수량_100':allValues.includes('100'),'단가_10000':allValues.includes('10,000')||allValues.includes('10000'),'공급가액_1000000':allValues.includes('1,000,000')||allValues.includes('1000000')};R.checks=checks;const matched=Object.values(checks).filter(Boolean).length;R.matched=matched;R.total=Object.keys(checks).length;R.ok=true;R.info=matched>=2?'pass: '+matched+'/'+R.total+' fields matched':'warn: only '+matched+'/'+R.total+' fields matched';return JSON.stringify(R);})()",
"timeout": 15000
},
{
"id": 24,
"name": "[READ] 견적서 출력 버튼 존재 확인",
"phase": "READ",
"action": "evaluate",
"script": "(async()=>{const R={phase:'PRINT_CHECK'};const printBtn=Array.from(document.querySelectorAll('button,a')).find(b=>{const t=b.innerText?.trim()||'';return(t.includes('출력')||t.includes('인쇄')||t.includes('PDF')||t.includes('견적서'))&&b.offsetParent!==null;});R.printBtnExists=!!printBtn;if(printBtn){R.btnText=printBtn.innerText?.trim().substring(0,30);}R.ok=true;R.info=printBtn?'pass: print button found ('+R.btnText+')':'warn: print/PDF button not found';return JSON.stringify(R);})()"
},
{
"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] 수량 변경: 100 → 150",
"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,'150');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] 재계산 검증: 150×10,000=1,500,000 / VAT 150,000 / 합계 1,650,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.hasSupply1500000=allVals.includes('1,500,000')||allVals.includes('1500000');R.hasVat150000=allVals.includes('150,000')||allVals.includes('150000');R.hasTotal1650000=allVals.includes('1,650,000')||allVals.includes('1650000');R.ok=true;R.info=[R.hasSupply1500000?'pass: supply=1,500,000':'warn: supply 1,500,000 not found',R.hasVat150000?'pass: vat=150,000':'warn: vat 150,000 not found',R.hasTotal1650000?'pass: total=1,650,000':'warn: total 1,650,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=>{const t=b.innerText?.trim()||'';return(t==='저장'||t==='수정'||/저장|수정/.test(t))&&b.offsetParent!==null&&!b.disabled;});if(!saveBtn){R.info='저장 버튼 없음 - 스킵';R.ok=true;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/quotations", "description": "견적 목록 조회" },
{ "method": "POST", "endpoint": "/api/v1/quotations", "description": "견적 등록" },
{ "method": "GET", "endpoint": "/api/v1/quotations/{id}", "description": "견적 상세 조회" },
{ "method": "PUT", "endpoint": "/api/v1/quotations/{id}", "description": "견적 수정" },
{ "method": "DELETE", "endpoint": "/api/v1/quotations/{id}", "description": "견적 삭제" }
],
"requiredVerifications": [
{ "id": 2, "name": "등록/저장 버튼", "steps": [15, 28], "criteria": "API 호출 + 성공 토스트 + 데이터 반영" },
{ "id": 3, "name": "계산 검증", "steps": [14, 27], "criteria": "수량×단가=공급가액, VAT 10%, 합계" },
{ "id": 4, "name": "견적서 출력", "steps": [24], "criteria": "출력/PDF 버튼 존재 확인" },
{ "id": 5, "name": "목업 페이지 감지", "steps": [4], "criteria": "견적 목록, 등록 버튼, 필터 존재" },
{ "id": 6, "name": "삭제 기능", "steps": [30, 31], "criteria": "DELETE API + 목록에서 제거" }
],
"rollbackPlan": {
"onCreateFail": "모달 닫기",
"onUpdateFail": "테스트 견적 수동 삭제 필요",
"onDeleteFail": "테스트 견적 수동 삭제 필요",
"cleanupRequired": "E2E_TEST_ 접두사 견적은 테스트 데이터"
}
}