refactor: production-work-order, vendor-management 시나리오 고도화 (combobox/date/API검증/섹션검증/네거티브검색)

This commit is contained in:
김보곤
2026-03-01 19:08:58 +09:00
parent 18463fbcca
commit 26226b5de2
2 changed files with 351 additions and 579 deletions

View File

@@ -1,19 +1,13 @@
{ {
"enabled": true, "enabled": true,
"id": "production-work-order", "id": "production-work-order",
"name": "작업지시 관리 테스트", "name": "작업지시 CRUD + 필드검증 + API확인: 생산관리",
"version": "2.0.0",
"screenshotPolicy": { "screenshotPolicy": {
"onErrorOnly": true, "captureOnFail": true,
"captureOn": [ "captureOnPass": false
"error",
"fail",
"timeout",
"404",
"500",
"blocked"
]
}, },
"description": "생산관리 > 작업지시 관리 메뉴의 작업지시 조회/등록/수정/삭제 전체 CRUD 테스트", "description": "생산관리 > 작업지시 관리 메뉴의 작업지시 CRUD 전체 흐름 + combobox/date picker + 상세 필드 대조 + API 검증",
"baseUrl": "https://dev.codebridge-x.com", "baseUrl": "https://dev.codebridge-x.com",
"menuNavigation": { "menuNavigation": {
"level1": "생산관리", "level1": "생산관리",
@@ -23,15 +17,12 @@
"closeOtherMenus": true "closeOtherMenus": true
}, },
"auth": { "auth": {
"username": "TestUser5", "role": "admin"
"password": "password123!"
}, },
"testData": { "testData": {
"create": { "create": {
"orderNumber": "E2E_TEST_작업지시", "orderNumber": "E2E_TEST_작업지시",
"itemName": "테스트품목",
"quantity": "500", "quantity": "500",
"dueDate": "2026-02-10",
"memo": "E2E 자동화 테스트 작업지시" "memo": "E2E 자동화 테스트 작업지시"
}, },
"update": { "update": {
@@ -42,28 +33,26 @@
"steps": [ "steps": [
{ {
"id": 1, "id": 1,
"name": "메뉴 진입: 생산관리 > 작업지시 관리", "name": "[생산관리 > 작업지시 관리] 페이지 로드 대기",
"action": "menu_navigate", "action": "wait",
"level1": "생산관리", "timeout": 5000
"level2": "작업지시 관리",
"expected": {
"url_contains": "/production/work-orders",
"visible": [
"작업지시"
]
}
}, },
{ {
"id": 2, "id": 2,
"name": "URL 검증", "name": "[생산관리 > 작업지시 관리] ts 초기화 + API 모니터링",
"action": "verify_url", "action": "evaluate",
"expected": { "script": "(()=>{try{sessionStorage.removeItem('__E2E_TS__');}catch(e){}delete window.__E2E_TS__;window.__CONSOLE_ERRORS__=[];const origErr=console.error;console.error=function(){window.__CONSOLE_ERRORS__.push(Array.from(arguments).join(' ').substring(0,200));origErr.apply(console,arguments);};return JSON.stringify({ok:true,cleared:true});})()",
"url_contains": "/production/work-orders" "timeout": 3000
}
}, },
{ {
"id": 3, "id": 3,
"name": "필수 검증 #5: 목업 페이지 감지", "name": "[생산관리 > 작업지시 관리] 테이블 로드 대기",
"action": "wait_for_table",
"timeout": 20000
},
{
"id": 4,
"name": "[생산관리 > 작업지시 관리] 목업 페이지 감지",
"action": "verify_not_mockup", "action": "verify_not_mockup",
"checks": [ "checks": [
"작업지시 목록 표시", "작업지시 목록 표시",
@@ -72,15 +61,9 @@
], ],
"expected": "정상 페이지 (목업 아님)" "expected": "정상 페이지 (목업 아님)"
}, },
{
"id": 4,
"name": "통계 카드 확인",
"action": "evaluate",
"script": "(() => {\n const cards = document.querySelectorAll('[class*=\"card\"], [class*=\"Card\"], [class*=\"stat\"], [class*=\"Stat\"], [class*=\"summary\"]');\n const texts = Array.from(cards).map(c => c.innerText?.substring(0, 30)).filter(Boolean);\n return texts.length > 0 ? 'Stats: ' + texts.length + ' cards found' : 'No stat cards (ok)';\n })()"
},
{ {
"id": 5, "id": 5,
"name": "작업지시 테이블 구조 확인", "name": "[생산관리 > 작업지시 관리] 테이블 구조 확인",
"action": "verify_table", "action": "verify_table",
"checks": [ "checks": [
"작업지시번호 컬럼", "작업지시번호 컬럼",
@@ -93,266 +76,211 @@
}, },
{ {
"id": 6, "id": 6,
"name": "목록 필터 테스트", "name": "[생산관리 > 작업지시 관리] [CREATE] 등록 버튼 클릭",
"action": "evaluate", "action": "evaluate",
"script": "(() => {\n const selects = document.querySelectorAll('select, [role=\"combobox\"], button[class*=\"select\"], button[class*=\"Select\"]');\n if (selects.length > 0) {\n return 'Filters found: ' + selects.length;\n }\n return 'No filter dropdowns (ok)';\n })()" "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CREATE_OPEN'};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,
"phase": "CREATE",
"critical": true
}, },
{ {
"id": 7, "id": 7,
"name": "검색 기능 테스트", "name": "[생산관리 > 작업지시 관리] [CREATE] 등록 폼 로드 대기",
"action": "click_if_exists", "action": "wait",
"target": "input[placeholder*='검색']", "timeout": 2000
"value": "테스트",
"expected": {
"data_filtered": true
}
}, },
{ {
"id": 8, "id": 8,
"phase": "CREATE", "name": "[생산관리 > 작업지시 관리] [CREATE] ts 생성 + 작업지시번호/메모 입력",
"name": "[CREATE] 작업지시 등록 버튼 클릭", "action": "evaluate",
"action": "click_if_exists", "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__||(()=>{const n=new Date();const p=v=>v.toString().padStart(2,'0');return 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:'BASIC_INPUT',ts};const formArea=document.querySelector('main')||document.querySelector('[class*=\"content\"]')||document.body;const inputs=Array.from(formArea.querySelectorAll('input[type=\"text\"],input[type=\"number\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);R.inputCount=inputs.length;const orderInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item],[class*=row]')?.innerText||'';return ph.includes('작업지시')||nm.includes('order')||nm.includes('workOrder')||lbl.includes('작업지시번호');});if(orderInput){sv(orderInput,'E2E_TEST_WO_'+ts);R.orderFilled=true;await w(200);}const memoInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item],[class*=row]')?.innerText||'';return ph.includes('메모')||ph.includes('비고')||nm.includes('memo')||nm.includes('note')||nm.includes('remark')||lbl.includes('메모')||lbl.includes('비고')||i.tagName==='TEXTAREA';});if(memoInput){sv(memoInput,'E2E_TEST_작업지시_'+ts);R.memoFilled=true;await w(200);}R.ok=true;return JSON.stringify(R);})()",
"target": "button:has-text('등록'), button:has-text('작업지시 등록'), button:has-text('추가'), button:has-text('신규'), button:has-text('작성')", "timeout": 15000,
"expected": { "phase": "CREATE"
"modal": true,
"modalTitle": "작업지시 등록"
}
}, },
{ {
"id": 9, "id": 9,
"phase": "CREATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 품목 combobox 선택",
"name": "[CREATE] 작업지시 정보 입력", "action": "evaluate",
"action": "click_if_exists", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'ITEM_COMBO'};const formArea=document.querySelector('main')||document.querySelector('[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 itemCombo=combos.find(b=>{const lbl=b.closest('[class*=\"field\"],[class*=\"Field\"],label,[class*=\"row\"],[class*=\"form\"]');return lbl&&(lbl.innerText.includes('품목')||lbl.innerText.includes('제품'));});const targetCombo=itemCombo||combos[0];if(targetCombo){document.body.click();await w(100);targetCombo.scrollIntoView({block:'center'});targetCombo.click();await w(600);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opts=lb.querySelectorAll('[role=\"option\"]');R.optionCount=opts.length;if(opts.length>0){opts[0].click();R.selected=opts[0].innerText?.trim().substring(0,30);await w(400);}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);R.noListbox=true;}}else{R.noCombo=true;}R.ok=true;return JSON.stringify(R);})()",
"note": "작업지시 등록 폼 필드 존재 확인 (등록 버튼 미존재 시 폼 없음, soft check)", "timeout": 15000,
"target": "form input, [role=\"dialog\"] input, .modal input" "phase": "CREATE"
}, },
{ {
"id": 10, "id": 10,
"phase": "CREATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 수량 입력 (500)",
"name": "[CREATE] 필수 검증 #2: 등록 저장", "action": "evaluate",
"action": "click_if_exists", "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 R={phase:'QTY_INPUT'};const formArea=document.querySelector('main')||document.querySelector('[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],[class*=row]')?.innerText||'';return ph.includes('수량')||nm.includes('quantity')||nm.includes('qty')||lbl.includes('수량');});if(qtyInput){sv(qtyInput,'500');R.qtyFilled=true;R.qtyValue=qtyInput.value;await w(300);}else{R.warn='수량 입력 필드 미발견';}R.ok=true;return JSON.stringify(R);})()",
"target": "button:has-text('저장'), button:has-text('등록'), button:has-text('확인'), button:has-text('추가')", "timeout": 10000,
"verify": { "phase": "CREATE"
"url_maintained": true,
"no_error_page": true,
"api_call": "POST /api/v1/work-orders",
"toast": "등록|완료|성공"
},
"expected": "작업지시 등록 완료"
}, },
{ {
"id": 11, "id": 11,
"phase": "CREATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 납기일 date picker 선택",
"name": "[CREATE] 저장 완료 토스트 확인", "action": "evaluate",
"action": "verify_toast", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DATE_PICK'};const formArea=document.querySelector('main')||document.querySelector('[class*=\"content\"]')||document.body;const dateButtons=Array.from(formArea.querySelectorAll('button')).filter(b=>(b.innerText?.trim()==='날짜 선택'||b.querySelector('svg[class*=\"calendar\"]')||b.getAttribute('aria-label')?.includes('날짜'))&&b.offsetParent!==null);R.dateBtnCount=dateButtons.length;const dateLabelBtn=dateButtons.find(b=>{const lbl=b.closest('[class*=\"field\"],[class*=\"Field\"],label,[class*=\"row\"],[class*=\"form\"]');return lbl&&(lbl.innerText.includes('납기')||lbl.innerText.includes('기한')||lbl.innerText.includes('마감'));});const targetBtn=dateLabelBtn||dateButtons[0];if(targetBtn){targetBtn.scrollIntoView({block:'center'});await w(100);targetBtn.click();await w(600);if(!document.querySelector('table[class*=\"rdp\"],.rdp-month,[role=\"grid\"]')){targetBtn.click();await w(600);}const today=document.querySelector('[aria-selected=\"true\"]')||document.querySelector('button[name=\"day\"].bg-primary')||document.querySelector('.rdp-day_today button')||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();R.dateSelected=true;await w(300);}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));R.noToday=true;await w(200);}}else{R.noDateBtn=true;}R.ok=true;return JSON.stringify(R);})()",
"verify": { "timeout": 15000,
"contains": "등록|완료|성공|저장" "phase": "CREATE"
}
}, },
{ {
"id": 12, "id": 12,
"phase": "CREATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 기타 combobox 선택 (우선순위 등)",
"name": "[CREATE] 모달 닫기 확인", "action": "evaluate",
"action": "close_modal_if_open", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'OTHER_COMBOS'};const formArea=document.querySelector('main')||document.querySelector('[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.totalCombos=combos.length;let filled=0;for(let i=0;i<combos.length;i++){const cb=combos[i];const alreadySelected=cb.innerText?.trim()&&cb.innerText.trim()!=='선택'&&cb.innerText.trim()!=='선택하세요';if(alreadySelected){filled++;continue;}document.body.click();await w(100);cb.scrollIntoView({block:'center'});cb.click();await w(500);const lb=document.querySelector('[role=\"listbox\"]');if(lb){const opt=lb.querySelector('[role=\"option\"]');if(opt){opt.click();filled++;await w(300);}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}R.filledCombos=filled;R.ok=true;return JSON.stringify(R);})()",
"expected": "모달 닫힘" "timeout": 20000,
"phase": "CREATE"
}, },
{ {
"id": 13, "id": 13,
"phase": "CREATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 등록 클릭",
"name": "[CREATE] 등록 결과 확인", "action": "evaluate",
"action": "verify_detail", "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 toasts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');R.toast=toasts.length>0?Array.from(toasts).pop()?.innerText?.trim().substring(0,100):'';R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"search": "E2E_TEST_작업지시", "timeout": 20000,
"expected": { "phase": "CREATE"
"row_exists": true,
"contains": [
"E2E",
"500"
]
}
}, },
{ {
"id": 14, "id": 14,
"phase": "READ", "name": "[생산관리 > 작업지시 관리] [CREATE] API POST 검증",
"name": "[READ] 작업지시 상세 페이지 진입", "action": "evaluate",
"action": "click_if_exists", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'API_POST_CHECK'};const logs=window.__API_LOGS__||[];const posts=logs.filter(l=>l.method==='POST'&&l.status>=200&&l.status<300);R.postCount=posts.length;R.lastPost=posts.length>0?{url:posts[posts.length-1].url?.substring(0,80),status:posts[posts.length-1].status,duration:posts[posts.length-1].duration}:null;R.ok=posts.length>0;R.info=posts.length>0?'POST API '+posts[posts.length-1].status+' OK':'warn: POST API 미감지';return JSON.stringify(R);})()",
"target": "table tbody tr:has-text('E2E')", "timeout": 10000,
"expected": { "phase": "VERIFY"
"url_contains": "/production/work-orders/",
"visible": [
"작업지시 상세",
"수정",
"삭제"
]
}
}, },
{ {
"id": 15, "id": 15,
"phase": "READ", "name": "[생산관리 > 작업지시 관리] [CREATE] 모달 닫기 + 목록 복귀",
"name": "[READ] 상세 정보 확인", "action": "evaluate",
"action": "verify_detail", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'BACK_TO_LIST'};const modal=document.querySelector('[role=\"dialog\"],[aria-modal=\"true\"],[class*=\"modal\"]:not([class*=\"tooltip\"]),[class*=\"Modal\"]');if(modal&&modal.offsetParent!==null){const closeBtn=modal.querySelector('button[class*=\"close\"],[aria-label=\"닫기\"],[aria-label=\"Close\"]')||Array.from(modal.querySelectorAll('button')).find(b=>/닫기|Close|취소|Cancel|확인/.test(b.innerText?.trim()));if(closeBtn){closeBtn.click();await w(1000);}}const onForm=location.search.includes('mode=new')||location.search.includes('mode=edit')||location.search.includes('mode=view')||/\\/(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);}}R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"checks": [ "timeout": 15000,
"작업지시번호: E2E_TEST_작업지시", "phase": "CREATE"
"수량: 500",
"납기일: 2026-02-10"
],
"expected": "입력한 데이터와 일치"
}, },
{ {
"id": 16, "id": 16,
"phase": "UPDATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 목록 안정화 대기",
"name": "[UPDATE] 수정 모드 진입", "action": "wait",
"action": "click_if_exists", "timeout": 2000
"target": "button:has-text('수정')",
"expected": {
"url_contains": "mode=edit",
"fields_editable": true
}
}, },
{ {
"id": 17, "id": 17,
"phase": "UPDATE", "name": "[생산관리 > 작업지시 관리] [CREATE] 목록에서 등록 확인 + 상태 컬럼 검증",
"name": "[UPDATE] 수량 수정", "action": "evaluate",
"action": "click_if_exists", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const ts=window.__E2E_TS__||sessionStorage.getItem('__E2E_TS__')||'';const R={phase:'VERIFY_CREATE'};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_'));}R.found=!!found;if(found){const cells=found.querySelectorAll('td');const cellTexts=Array.from(cells).map(c=>c.innerText?.trim());R.rowData=cellTexts.join(' | ').substring(0,200);const statusKeywords=['대기','등록','진행','예정','준비','미착수','생성'];const hasStatus=cellTexts.some(t=>statusKeywords.some(kw=>t.includes(kw)));R.statusFound=hasStatus;R.statusInfo=hasStatus?'상태 컬럼 확인':'상태 컬럼 미감지 (확인 필요)';}R.ok=R.found||R.rowCount>0;return JSON.stringify(R);})()",
"target": "input[name*='quantity'], input[placeholder*='수량']" "timeout": 20000,
"phase": "VERIFY"
}, },
{ {
"id": 18, "id": 18,
"phase": "UPDATE", "name": "[생산관리 > 작업지시 관리] [READ] E2E 행 클릭 → 상세 진입",
"name": "[UPDATE] 메모 수정", "action": "evaluate",
"action": "click_if_exists", "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];R.usedTestRow=!!testRow;if(targetRow){const cells=targetRow.querySelectorAll('td');const captured={};const colNames=['checkbox','no','orderNo','itemName','qty','dueDate','status','priority','memo'];cells.forEach((cell,i)=>{const key=colNames[i]||('col'+i);captured[key]=cell.innerText?.trim()||'';});window.__E2E_CAPTURED__=captured;R.captured=captured;targetRow.click();await w(3000);}R.detailUrl=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"target": "textarea[name*='memo'], input[placeholder*='메모']" "timeout": 15000,
"phase": "READ"
}, },
{ {
"id": 19, "id": 19,
"phase": "UPDATE", "name": "[생산관리 > 작업지시 관리] [READ] 상세 페이지 로드 대기",
"name": "[UPDATE] 필수 검증 #2: 수정 저장", "action": "wait",
"action": "click_if_exists", "timeout": 2000
"target": "button:has-text('저장')",
"verify": {
"url_maintained": true,
"no_error_page": true,
"api_call": "PUT /api/v1/work-orders/",
"toast": "수정|완료|성공"
},
"expected": "수정 완료"
}, },
{ {
"id": 20, "id": 20,
"phase": "UPDATE", "name": "[생산관리 > 작업지시 관리] [READ] 상세 필드 1:1 대조 검증",
"name": "[UPDATE] 수정 완료 토스트 확인", "action": "evaluate",
"action": "verify_toast", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DETAIL_VERIFY'};const cap=window.__E2E_CAPTURED__||{};R.hasCaptured=Object.keys(cap).length>0;if(!R.hasCaptured){R.error='캡처 데이터 없음';R.ok=false;return JSON.stringify(R);}const norm=s=>(s||'').replace(/[,\\s]/g,'').trim();const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input,textarea,select')).filter(i=>i.offsetParent!==null);const allValues=[...inputs.map(i=>i.value)];const matches={};const checks=['orderNo','itemName','qty','dueDate','status','priority','memo'];checks.forEach(key=>{const val=cap[key];if(!val||val==='')return;const nv=norm(val);const found=pageText.includes(val)||pageText.includes(nv)||norm(pageText).includes(nv)||allValues.some(v=>v?.includes(val)||norm(v)===nv||norm(v).includes(nv));matches[key]={expected:val,found};});R.matches=matches;const matchCount=Object.values(matches).filter(m=>m.found).length;const totalChecks=Object.keys(matches).length;R.matchCount=matchCount;R.totalChecks=totalChecks;R.matchRate=totalChecks>0?Math.round(matchCount/totalChecks*100)+'%':'N/A';R.ok=matchCount>=Math.max(1,Math.ceil(totalChecks*0.4));return JSON.stringify(R);})()",
"verify": { "timeout": 15000,
"contains": "수정|완료|성공|저장" "phase": "VERIFY"
}
}, },
{ {
"id": 21, "id": 21,
"phase": "UPDATE", "name": "[생산관리 > 작업지시 관리] [UPDATE] 수정 모드 진입",
"name": "[UPDATE] 수정 결과 확인", "action": "evaluate",
"action": "verify_detail", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'EDIT_ENTER'};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);})()",
"checks": [ "timeout": 15000,
"수량: 600", "phase": "UPDATE"
"메모: E2E 수정된 작업지시"
],
"expected": "수정된 데이터 반영"
}, },
{ {
"id": 22, "id": 22,
"phase": "DELETE", "name": "[생산관리 > 작업지시 관리] [UPDATE] 수량 500→600 수정",
"name": "[DELETE] 삭제 버튼 클릭", "action": "evaluate",
"action": "click_if_exists", "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 inputs=Array.from(document.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],[class*=row]')?.innerText||'';return(ph.includes('수량')||nm.includes('quantity')||nm.includes('qty')||lbl.includes('수량'))&&(i.value==='500'||i.value==='500.00');});if(qtyInput){sv(qtyInput,'600');R.qtyUpdated=true;R.newValue=qtyInput.value;await w(300);}else{const fallback=inputs.find(i=>i.value==='500'||i.value==='500.00');if(fallback){sv(fallback,'600');R.qtyUpdated=true;R.usedFallback=true;await w(300);}else{R.warn='수량 입력 필드(500) 미발견';}}R.ok=true;return JSON.stringify(R);})()",
"target": "button:has-text('삭제')", "timeout": 10000,
"expected": { "phase": "UPDATE"
"confirm_dialog": true,
"dialog_message": "삭제|정말"
}
}, },
{ {
"id": 23, "id": 23,
"phase": "DELETE", "name": "[생산관리 > 작업지시 관리] [UPDATE] 메모 수정",
"name": "[DELETE] 필수 검증 #6: 삭제 확인", "action": "evaluate",
"action": "click_if_exists", "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:'UPDATE_MEMO'};const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const memoInput=inputs.find(i=>{const ph=i.placeholder||'';const nm=i.name||'';const lbl=i.closest('[class*=field],[class*=Field],[class*=form-item],[class*=row]')?.innerText||'';return ph.includes('메모')||ph.includes('비고')||nm.includes('memo')||nm.includes('note')||nm.includes('remark')||lbl.includes('메모')||lbl.includes('비고')||i.tagName==='TEXTAREA';});if(memoInput){sv(memoInput,'E2E_수정완료_작업지시_'+ts);R.memoUpdated=true;await w(300);}else{R.warn='메모 필드 미발견';}R.ok=true;return JSON.stringify(R);})()",
"target": "button:has-text('확인'), button:has-text('삭제')", "timeout": 10000,
"verify": { "phase": "UPDATE"
"api_call": "DELETE /api/v1/work-orders/",
"toast": "삭제|완료|성공",
"redirect": "/production/work-orders"
},
"expected": "삭제 완료 및 목록 복귀"
}, },
{ {
"id": 24, "id": 24,
"phase": "DELETE", "name": "[생산관리 > 작업지시 관리] [UPDATE] 저장 클릭",
"name": "[DELETE] 삭제 결과 확인", "action": "evaluate",
"action": "verify_detail", "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 toasts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');R.toast=toasts.length>0?Array.from(toasts).pop()?.innerText?.trim().substring(0,100):'';R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"search": "E2E 수정된 작업지시", "timeout": 20000,
"expected": { "phase": "UPDATE"
"row_exists": false,
"message": "테스트 작업지시가 목록에서 제거됨"
}
}, },
{ {
"id": 25, "id": 25,
"name": "콘솔 에러 확인", "name": "[생산관리 > 작업지시 관리] [UPDATE] API PUT 검증",
"action": "verify_element", "action": "evaluate",
"target": "body" "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(500);const R={phase:'API_PUT_CHECK'};const logs=window.__API_LOGS__||[];const puts=logs.filter(l=>(l.method==='PUT'||l.method==='PATCH')&&l.status>=200&&l.status<300);R.putCount=puts.length;R.lastPut=puts.length>0?{url:puts[puts.length-1].url?.substring(0,80),status:puts[puts.length-1].status,duration:puts[puts.length-1].duration}:null;R.ok=puts.length>0;R.info=puts.length>0?'PUT/PATCH API '+puts[puts.length-1].status+' OK':'warn: PUT/PATCH API 미감지';return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 26,
"name": "[생산관리 > 작업지시 관리] [UPDATE] 수정 결과 확인 (600 반영)",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'VERIFY_UPDATE'};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.has600=allVals.includes('600');R.hasMemo=allVals.includes('E2E_수정완료');R.ok=true;R.info=[R.has600?'수량 600 확인':'수량 600 미감지',R.hasMemo?'메모 수정 확인':'메모 수정 미감지'].join(' | ');return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 27,
"name": "[생산관리 > 작업지시 관리] [DELETE] 삭제 실행",
"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:'DELETE'};const onDetail=/\\/(new|[0-9]+|[0-9a-f]{8,})/.test(location.pathname)||location.search.includes('mode=');if(!onDetail){const rows=Array.from(document.querySelectorAll('table tbody tr'));const row=rows.find(r=>r.innerText?.includes('E2E_TEST_'));if(row){row.click();await w(3000);}else{R.info='E2E 행 미발견 - 삭제 스킵';R.ok=true;return JSON.stringify(R);}}let delBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='삭제'&&b.offsetParent!==null);if(!delBtn){await w(2000);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.urlAfter=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"timeout": 30000,
"phase": "DELETE"
},
{
"id": 28,
"name": "[생산관리 > 작업지시 관리] [DELETE] API DELETE 검증",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(500);const R={phase:'API_DELETE_CHECK'};const logs=window.__API_LOGS__||[];const dels=logs.filter(l=>l.method==='DELETE'&&l.status>=200&&l.status<300);R.deleteCount=dels.length;R.lastDelete=dels.length>0?{url:dels[dels.length-1].url?.substring(0,80),status:dels[dels.length-1].status}:null;R.ok=dels.length>0;R.info=dels.length>0?'DELETE API '+dels[dels.length-1].status+' OK':'warn: DELETE API 미감지';return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 29,
"name": "[생산관리 > 작업지시 관리] [DELETE] 목록 복귀 + 삭제 확인",
"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_DELETE'};const onForm=location.search.includes('mode=')||/\\/(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 found=rows.find(r=>r.innerText?.includes(ts));R.stillExists=!!found;R.rowCount=rows.length;R.url=location.pathname+location.search;R.ok=!found;return JSON.stringify(R);})()",
"timeout": 15000,
"phase": "VERIFY"
},
{
"id": 30,
"name": "[생산관리 > 작업지시 관리] [FINAL] API 요약 + 콘솔 에러 확인",
"action": "evaluate",
"script": "(()=>{const R={phase:'FINAL_SUMMARY'};const logs=window.__API_LOGS__||[];R.apiSummary={total:logs.length,success:logs.filter(l=>l.ok||l.status<400).length,failed:logs.filter(l=>!l.ok&&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,methods:{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}};const errs=window.__CONSOLE_ERRORS__||[];R.consoleErrors=errs.length;R.errorSamples=errs.slice(0,3);R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
} }
], ],
"expectedAPIs": [ "expectedAPIs": [
{ { "method": "GET", "endpoint": "/api/v1/work-orders", "description": "작업지시 목록 조회" },
"method": "GET", { "method": "POST", "endpoint": "/api/v1/work-orders", "description": "작업지시 등록" },
"endpoint": "/api/v1/work-orders", { "method": "GET", "endpoint": "/api/v1/work-orders/{id}", "description": "작업지시 상세 조회" },
"description": "작업지시 목록 조회" { "method": "PUT", "endpoint": "/api/v1/work-orders/{id}", "description": "작업지시 수정" },
}, { "method": "DELETE", "endpoint": "/api/v1/work-orders/{id}", "description": "작업지시 삭제" }
{
"method": "POST",
"endpoint": "/api/v1/work-orders",
"description": "작업지시 등록"
},
{
"method": "GET",
"endpoint": "/api/v1/work-orders/{id}",
"description": "작업지시 상세 조회"
},
{
"method": "PUT",
"endpoint": "/api/v1/work-orders/{id}",
"description": "작업지시 수정"
},
{
"method": "DELETE",
"endpoint": "/api/v1/work-orders/{id}",
"description": "작업지시 삭제"
}
], ],
"requiredVerifications": [ "requiredVerifications": [
{ { "id": 2, "name": "등록/저장 버튼", "steps": [13, 14], "criteria": "API POST + 토스트 + 목록 반영" },
"id": 2, { "id": 5, "name": "목업 페이지 감지", "steps": [4], "criteria": "작업지시 목록, 등록 버튼, 필터 존재" },
"name": "등록/저장 버튼", { "id": 6, "name": "삭제 기능", "steps": [27, 28, 29], "criteria": "DELETE API + 목록에서 제거" }
"steps": [
7,
14
],
"criteria": "API 호출 + 성공 토스트 + 데이터 반영"
},
{
"id": 5,
"name": "목업 페이지 감지",
"steps": [
2
],
"criteria": "작업지시 목록, 등록 버튼, 필터 존재"
},
{
"id": 6,
"name": "삭제 기능",
"steps": [
16,
17,
18
],
"criteria": "DELETE API + 목록에서 제거"
}
], ],
"rollbackPlan": { "rollbackPlan": {
"onCreateFail": "모달 닫기", "onCreateFail": "모달 닫기",

View File

@@ -1,27 +1,17 @@
{ {
"id": "vendor-management", "id": "vendor-management",
"name": "거래처관리 테스트", "name": "거래처관리 검색/필터/상세/수정/복원 + 네거티브 + 섹션검증: 회계관리",
"version": "2.0.0",
"screenshotPolicy": { "screenshotPolicy": {
"onErrorOnly": true, "captureOnFail": true,
"captureOn": [ "captureOnPass": false
"error",
"fail",
"timeout",
"404",
"500",
"blocked"
]
}, },
"description": "회계관리 > 거래처관리 메뉴의 목록 조회, 필터, 검색, 상세 페이지 진입, 수정 및 저장 기능 테스트", "description": "회계관리 > 거래처관리 메뉴의 목록 조회, 필터, 네거티브 검색, 복합 필터, 상세 섹션별 필드 검증, 다중 필드 수정/복원 기능 테스트",
"baseUrl": "https://dev.codebridge-x.com", "baseUrl": "https://dev.codebridge-x.com",
"navigation": { "navigation": {
"targetUrl": "/accounting/vendors", "targetUrl": "/accounting/vendors",
"urlPattern": "/accounting/vendors", "urlPattern": "/accounting/vendors",
"menuHints": [ "menuHints": ["거래처관리", "거래처 관리", "회계관리"]
"거래처관리",
"거래처 관리",
"회계관리"
]
}, },
"menuNavigation": { "menuNavigation": {
"level1": "회계관리", "level1": "회계관리",
@@ -30,43 +20,12 @@
"searchWithinParent": true, "searchWithinParent": true,
"closeOtherMenus": true "closeOtherMenus": true
}, },
"menuNavigationEnhanced": {
"strategy": "scroll-and-search",
"description": "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지",
"level1": "회계관리",
"level2": "거래처관리",
"alternativeLevel1Names": [
"회계관리",
"회계 관리",
"Accounting"
],
"alternativeLevel2Names": [
"거래처관리",
"거래처 관리",
"Vendors"
],
"scrollConfig": {
"sidebarSelector": "nav, aside, [role='navigation'], .sidebar, #sidebar",
"menuItemSelector": "a, button, [role='menuitem'], [role='treeitem']",
"scrollStep": 200,
"maxScrollAttempts": 10,
"scrollDelay": 300
}
},
"auth": { "auth": {
"username": "TestUser5", "role": "admin"
"password": "password123!"
}, },
"notes": { "notes": {
"skip": [ "skip": ["등록 버튼 (추후 구현 예정)", "삭제 기능 (보류)"],
"등록 버튼 (추후 구현 예정)", "focus": ["네거티브 검색", "복합 필터", "섹션별 필드 검증", "다중 필드 수정/복원"],
"삭제 기능 (보류)"
],
"focus": [
"테이블 행 클릭 → 상세 페이지",
"수정 모드 진입",
"수정 후 저장"
],
"uiNotes": [ "uiNotes": [
"필터 드롭다운: Radix UI Select (button[role='combobox'])", "필터 드롭다운: Radix UI Select (button[role='combobox'])",
"체크박스: Radix UI Checkbox (button[role='checkbox'])", "체크박스: Radix UI Checkbox (button[role='checkbox'])",
@@ -76,418 +35,303 @@
"steps": [ "steps": [
{ {
"id": 1, "id": 1,
"name": "사이드바 메뉴 전체 펼치기", "name": "[회계관리 > 거래처관리] 사이드바 메뉴 전체 펼치기",
"description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'}); await new Promise(r=>setTimeout(r,300)); Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click(); await new Promise(r=>setTimeout(r,2000)); return 'menu expanded'; })()" "script": "(async () => { document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'}); await new Promise(r=>setTimeout(r,300)); Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click(); await new Promise(r=>setTimeout(r,2000)); return 'menu expanded'; })()"
}, },
{ {
"id": 2, "id": 2,
"name": "2단계 메뉴 진입: 회계관리 > 거래처관리", "name": "[회계관리 > 거래처관리] 2단계 메뉴 진입",
"description": "사이드바를 스크롤하며 회계관리 > 거래처관리 메뉴를 찾아 클릭",
"action": "menu_navigate", "action": "menu_navigate",
"level1": "회계관리", "level1": "회계관리",
"level2": "거래처관리", "level2": "거래처관리"
"expect": {
"url": "/accounting/vendors",
"pageTitle": "거래처관리",
"elements": [
"통계 카드",
"테이블",
"검색창"
]
},
"verification": [
"회계관리 메뉴가 펼쳐졌는지 확인",
"거래처관리 서브메뉴 클릭 성공",
"404 에러 없이 페이지 로드 완료"
]
}, },
{ {
"id": 3, "id": 3,
"name": "필수 검증 #5: 목업 페이지 감지", "name": "[회계관리 > 거래처관리] 페이지 로드 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 4,
"name": "[회계관리 > 거래처관리] ts 초기화 + 콘솔 에러 모니터링",
"action": "evaluate",
"script": "(()=>{try{sessionStorage.removeItem('__E2E_TS__');}catch(e){}delete window.__E2E_TS__;window.__CONSOLE_ERRORS__=[];const origErr=console.error;console.error=function(){window.__CONSOLE_ERRORS__.push(Array.from(arguments).join(' ').substring(0,200));origErr.apply(console,arguments);};return JSON.stringify({ok:true,cleared:true});})()",
"timeout": 3000
},
{
"id": 5,
"name": "[회계관리 > 거래처관리] 테이블 로드 대기",
"action": "wait_for_table",
"timeout": 20000
},
{
"id": 6,
"name": "[회계관리 > 거래처관리] 목업 페이지 감지",
"action": "verify_not_mockup", "action": "verify_not_mockup",
"checks": [ "checks": [
"검색 입력 필드 존재 (placeholder: 거래처명, 거래처코드, 사업자번호 검색...)", "검색 입력 필드 존재",
"필터 드롭다운 존재 (구분, 신용등급, 거래등급, 악성채권 - Radix combobox)", "필터 드롭다운 존재",
"테이블 데이터 표시", "테이블 데이터 표시",
"API 호출 확인" "API 호출 확인"
], ],
"expected": "정상 페이지 (목업 아님)" "expected": "정상 페이지 (목업 아님)"
}, },
{
"id": 4,
"name": "통계 카드 확인",
"action": "verify_elements",
"checks": [
"전체 거래처 카드 표시",
"매출 거래처 카드 표시",
"매입 거래처 카드 표시"
],
"expected": "3개 통계 카드 모두 표시"
},
{
"id": 5,
"name": "테이블 구조 확인",
"action": "verify_table",
"checks": [
"체크박스 컬럼 (Radix button[role='checkbox'])",
"번호 컬럼",
"구분 컬럼 (매출/매입/매입매출)",
"거래처명 컬럼",
"매입 결제일 컬럼",
"매출 결제일 컬럼",
"신용등급 컬럼",
"거래등급 컬럼",
"미수금 컬럼",
"악성채권 컬럼"
],
"expected": "10개 컬럼 존재"
},
{
"id": 6,
"name": "⚠️ 필수 검증: 검색 기능",
"description": "검색어 입력 후 테이블 데이터가 필터링되는지 확인",
"action": "evaluate",
"script": "(async () => { const beforeCount = document.querySelectorAll('table tbody tr').length; window.__e2e_beforeSearch = beforeCount; const inp = document.querySelector('input[placeholder*=\"검색\"]'); if(!inp) return 'search input not found'; const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(inp,'가우스'); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); await new Promise(r=>setTimeout(r,1000)); const afterCount = document.querySelectorAll('table tbody tr').length; return 'beforeSearch=' + beforeCount + ', afterSearch=' + afterCount + ', filtered=' + (afterCount < beforeCount); })()",
"verify": {
"searchApplied": true,
"tableContains": "가우스",
"dataFiltered": "검색어에 맞는 거래처만 필터링되어야 함"
}
},
{ {
"id": 7, "id": 7,
"name": "검색 결과 데이터 검증", "name": "[회계관리 > 거래처관리] 통계 카드 확인",
"description": "검색 결과의 각 행에 검색어가 포함되어 있는지 확인", "action": "evaluate",
"action": "verify_text", "script": "(()=>{const R={phase:'STATS'};const cards=document.querySelectorAll('[class*=\"card\"],[class*=\"Card\"],[class*=\"stat\"],[class*=\"Stat\"],[class*=\"summary\"]');const texts=Array.from(cards).map(c=>c.innerText?.substring(0,50)).filter(Boolean);R.cardCount=texts.length;R.cards=texts.slice(0,5);R.ok=texts.length>=2;R.info=texts.length>=2?texts.length+' 통계 카드 확인':'warn: 통계 카드 '+texts.length+'개';return JSON.stringify(R);})()",
"target": "table tbody", "timeout": 10000,
"text": "가우스", "phase": "VERIFY"
"verify": {
"allRowsContain": "가우스",
"verifyMethod": "테이블의 모든 행이 검색어 '가우스'를 포함하는지 확인"
}
}, },
{ {
"id": 8, "id": 8,
"name": "검색 초기화 및 복원 확인", "name": "[회계관리 > 거래처관리] 테이블 구조 확인",
"description": "검색어 삭제 후 전체 목록 복원 확인", "action": "verify_table",
"action": "evaluate", "checks": [
"script": "(async () => { const inp = document.querySelector('input[placeholder*=\"검색\"]'); if(inp){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; nset.call(inp,''); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); } await new Promise(r=>setTimeout(r,1000)); const c = document.querySelectorAll('table tbody tr').length; return 'restored rows=' + c + ', restored=' + (c >= (window.__e2e_beforeSearch||1)); })()", "번호 컬럼",
"verify": { "구분 컬럼",
"dataRestored": true, "거래처명 컬럼",
"rowCountRestored": "검색 전과 유사한 행 수로 복원" "신용등급 컬럼",
} "거래등급 컬럼",
"미수금 컬럼"
],
"expected": "거래처 테이블 컬럼 정상 표시"
}, },
{ {
"id": 9, "id": 9,
"name": "구분 필터 테스트 (매출)", "name": "[회계관리 > 거래처관리] [SEARCH] 가우스 검색",
"description": "첫 번째 Radix combobox를 클릭하여 '매출' 옵션 선택",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const cbs = document.querySelectorAll('button[role=\"combobox\"]'); if(!cbs[0]) return 'combobox not found'; cbs[0].click(); await new Promise(r=>setTimeout(r,500)); const opt = Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='매출'); if(opt){ opt.click(); await new Promise(r=>setTimeout(r,1000)); return 'selected 매출, rows=' + document.querySelectorAll('table tbody tr').length; } return 'option 매출 not found'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SEARCH_POSITIVE'};const beforeCount=document.querySelectorAll('table tbody tr').length;window.__e2e_beforeSearch=beforeCount;R.beforeCount=beforeCount;const inp=document.querySelector('input[placeholder*=\"검색\"]');if(!inp){R.error='검색 입력 필드 없음';R.ok=false;return JSON.stringify(R);}const nset=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;nset.call(inp,'가우스');inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));await w(1500);const afterCount=document.querySelectorAll('table tbody tr').length;R.afterCount=afterCount;R.filtered=afterCount<=beforeCount;const rows=Array.from(document.querySelectorAll('table tbody tr'));const allContain=rows.every(r=>r.innerText?.includes('가우스'));R.allRowsContainKeyword=allContain;R.ok=afterCount>0;return JSON.stringify(R);})()",
"expected": "매출 거래처만 필터링" "timeout": 15000,
"phase": "SEARCH"
}, },
{ {
"id": 10, "id": 10,
"name": "구분 필터 초기화", "name": "[회계관리 > 거래처관리] [SEARCH] 검색 결과 행 데이터 검증",
"description": "구분 필터를 '전체'로 되돌리기", "action": "verify_text",
"action": "evaluate", "target": "table tbody",
"script": "(async () => { const cbs = document.querySelectorAll('button[role=\"combobox\"]'); if(!cbs[0]) return 'combobox not found'; cbs[0].click(); await new Promise(r=>setTimeout(r,500)); const opt = Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='전체'); if(opt){ opt.click(); await new Promise(r=>setTimeout(r,1000)); return 'selected 전체, rows=' + document.querySelectorAll('table tbody tr').length; } return 'option 전체 not found'; })()", "text": "가우스"
"expected": "전체 데이터 다시 표시"
}, },
{ {
"id": 11, "id": 11,
"name": "테이블 행 클릭 - 상세 페이지 이동", "name": "[회계관리 > 거래처관리] [SEARCH] 검색 초기화",
"description": "목록 페이지에서 첫 번째 행을 클릭하여 상세 페이지로 이동",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const url = window.location.href; if(!url.includes('/accounting/vendors') || url.includes('mode=')) return 'NOT on list page: ' + url; const rows = document.querySelectorAll('table tbody tr'); if(rows.length===0) return 'no rows'; const testRow = Array.from(rows).find(r=>r.innerText?.includes('E2E_TEST_')); const targetRow = testRow || rows[0]; targetRow.click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked row (testRow=' + !!testRow + '), url=' + window.location.href; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SEARCH_RESET'};const inp=document.querySelector('input[placeholder*=\"검색\"]');if(inp){const nset=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;nset.call(inp,'');inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));}await w(1500);const c=document.querySelectorAll('table tbody tr').length;R.restoredCount=c;R.restored=c>=(window.__e2e_beforeSearch||1);R.ok=true;return JSON.stringify(R);})()",
"expected": "거래처 상세 페이지로 이동" "timeout": 10000,
"phase": "SEARCH"
}, },
{ {
"id": 12, "id": 12,
"name": "상세 페이지 - URL 확인", "name": "[회계관리 > 거래처관리] [SEARCH-NEG] 존재하지 않는 키워드 검색",
"action": "verify_url", "action": "evaluate",
"target": "/accounting/vendors/\\d+", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SEARCH_NEGATIVE'};const inp=document.querySelector('input[placeholder*=\"검색\"]');if(!inp){R.error='검색 입력 필드 없음';R.ok=false;return JSON.stringify(R);}const nset=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;nset.call(inp,'ZZZZNONEXIST99');inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));await w(1500);const rows=document.querySelectorAll('table tbody tr');const noData=document.body.innerText.includes('데이터가 없')||document.body.innerText.includes('결과가 없')||document.body.innerText.includes('데이터 없')||document.body.innerText.includes('No data');R.rowCount=rows.length;R.noDataMessage=noData;R.ok=(rows.length===0||noData||(rows.length===1&&rows[0].innerText?.includes('데이터')));R.info=(rows.length===0||noData)?'pass: 0 results confirmed':'warn: found '+rows.length+' rows';return JSON.stringify(R);})()",
"checks": [ "timeout": 15000,
"URL에 거래처 ID 포함 (/accounting/vendors/{id})" "phase": "SEARCH"
],
"expected": "URL 정상 전달"
}, },
{ {
"id": 13, "id": 13,
"name": "상세 페이지 - 헤더 확인", "name": "[회계관리 > 거래처관리] [SEARCH-NEG] 검색 초기화 복원",
"action": "verify_elements", "action": "evaluate",
"checks": [ "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SEARCH_RESTORE'};const inp=document.querySelector('input[placeholder*=\"검색\"]');if(inp){const nset=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;nset.call(inp,'');inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));}await w(1500);const c=document.querySelectorAll('table tbody tr').length;R.restoredCount=c;R.ok=c>0;return JSON.stringify(R);})()",
"거래처 상세 타이틀", "timeout": 10000,
"목록 버튼 존재", "phase": "SEARCH"
"삭제 버튼 존재",
"수정 버튼 존재"
],
"expected": "상세 페이지 헤더 정상 표시"
}, },
{ {
"id": 14, "id": 14,
"name": "상세 페이지 - 기본 정보 카드 확인", "name": "[회계관리 > 거래처관리] [FILTER] 구분 필터 매출 선택",
"action": "verify_detail", "action": "evaluate",
"checks": [ "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'FILTER_SALES'};const cbs=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null);R.comboCount=cbs.length;if(!cbs[0]){R.warn='combobox not found';R.ok=true;return JSON.stringify(R);}cbs[0].click();await w(500);const opt=Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='매출');if(opt){opt.click();await w(1500);const rows=Array.from(document.querySelectorAll('table tbody tr'));R.rowCount=rows.length;const salesRows=rows.filter(r=>{const cells=r.querySelectorAll('td');return Array.from(cells).some(c=>c.innerText?.trim()==='매출');});R.salesRowCount=salesRows.length;R.allAreSales=salesRows.length===rows.length||rows.length===0;R.info=R.allAreSales?'모든 행이 매출 타입':'일부 비매출 행 존재 ('+salesRows.length+'/'+rows.length+')';}else{R.warn='매출 옵션 없음';}R.ok=true;return JSON.stringify(R);})()",
"visible_text:사업자등록번호", "timeout": 15000,
"visible_text:거래처코드", "phase": "FILTER"
"visible_text:거래처명",
"visible_text:대표자명",
"visible_text:업태",
"visible_text:업종"
],
"expected": "기본 정보 모두 표시 (읽기 전용)"
}, },
{ {
"id": 15, "id": 15,
"name": "상세 페이지 - 연락처 정보 확인", "name": "[회계관리 > 거래처관리] [FILTER+SEARCH] 매출 필터 + 가우스 검색 복합 테스트",
"action": "verify_detail", "action": "evaluate",
"checks": [ "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'FILTER_PLUS_SEARCH'};const inp=document.querySelector('input[placeholder*=\"검색\"]');if(inp){const nset=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;nset.call(inp,'가우스');inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));}await w(1500);const rows=Array.from(document.querySelectorAll('table tbody tr'));R.rowCount=rows.length;const allMatch=rows.every(r=>{const text=r.innerText||'';return text.includes('가우스');});R.allContainKeyword=allMatch;const allSales=rows.every(r=>{const cells=r.querySelectorAll('td');return Array.from(cells).some(c=>c.innerText?.trim()==='매출');});R.allAreSalesType=allSales;R.ok=true;R.info='rows='+rows.length+', allKeyword='+allMatch+', allSales='+allSales;return JSON.stringify(R);})()",
"visible_text:주소", "timeout": 15000,
"visible_text:전화번호", "phase": "FILTER"
"visible_text:모바일",
"visible_text:팩스",
"visible_text:이메일"
],
"expected": "연락처 정보 모두 표시"
}, },
{ {
"id": 16, "id": 16,
"name": "상세 페이지 - 담당자 정보 확인", "name": "[회계관리 > 거래처관리] [FILTER] 구분 필터 전체로 복원 + 검색 초기화",
"action": "verify_detail", "action": "evaluate",
"checks": [ "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'FILTER_RESET'};const inp=document.querySelector('input[placeholder*=\"검색\"]');if(inp){const nset=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;nset.call(inp,'');inp.dispatchEvent(new Event('input',{bubbles:true}));inp.dispatchEvent(new Event('change',{bubbles:true}));}await w(500);const cbs=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>b.offsetParent!==null);if(cbs[0]){cbs[0].click();await w(500);const opt=Array.from(document.querySelectorAll('[role=\"option\"]')).find(o=>o.innerText?.trim()==='전체');if(opt){opt.click();await w(1000);}}await w(1000);R.rowCount=document.querySelectorAll('table tbody tr').length;R.ok=R.rowCount>0;return JSON.stringify(R);})()",
"visible_text:담당자명", "timeout": 15000,
"visible_text:담당자 전화", "phase": "FILTER"
"visible_text:시스템 관리자"
],
"expected": "담당자 정보 모두 표시"
}, },
{ {
"id": 17, "id": 17,
"name": "상세 페이지 - 회사 정보 확인", "name": "[회계관리 > 거래처관리] [DETAIL] 첫 행 셀 값 캡처",
"action": "verify_detail", "action": "evaluate",
"checks": [ "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CAPTURE'};await w(500);const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;if(rows.length===0){R.error='테이블에 데이터 없음';R.ok=false;return JSON.stringify(R);}const firstRow=rows[0];const cells=firstRow.querySelectorAll('td');const captured={};const colNames=['checkbox','no','type','vendorName','purchasePayDay','salesPayDay','creditGrade','tradeGrade','receivable','badDebt'];cells.forEach((cell,i)=>{const key=colNames[i]||('col'+i);captured[key]=cell.innerText?.trim()||'';});window.__E2E_CAPTURED__=captured;R.captured=captured;R.ok=true;return JSON.stringify(R);})()",
"visible_text:회사 로고 영역", "timeout": 15000,
"visible_text:매입 결제일", "phase": "CAPTURE"
"visible_text:매출 결제일"
],
"expected": "회사 정보 모두 표시"
}, },
{ {
"id": 18, "id": 18,
"name": "상세 페이지 - 신용/거래 정보 확인", "name": "[회계관리 > 거래처관리] [DETAIL] 첫 행 클릭 → 상세 진입",
"action": "verify_detail", "action": "evaluate",
"checks": [ "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DETAIL_ENTER'};const rows=document.querySelectorAll('table tbody tr');if(rows.length===0){R.error='행 없음';R.ok=false;return JSON.stringify(R);}rows[0].click();await w(2500);R.detailUrl=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()",
"visible_text:신용등급", "timeout": 15000,
"visible_text:거래등급", "phase": "READ"
"visible_text:세금계산서 이메일",
"visible_text:입금계좌 은행",
"visible_text:계좌",
"visible_text:예금주"
],
"expected": "신용/거래 정보 모두 표시"
}, },
{ {
"id": 19, "id": 19,
"name": "상세 페이지 - 추가 정보 확인", "name": "[회계관리 > 거래처관리] [DETAIL] 상세 페이지 로드 대기",
"action": "verify_detail", "action": "wait",
"checks": [ "timeout": 2000
"visible_text:미수금",
"visible_text:악성채권 금액",
"visible_text:악성채권 상태"
],
"expected": "추가 정보 모두 표시"
}, },
{ {
"id": 20, "id": 20,
"name": "상세 페이지 - 메모 섹션 확인", "name": "[회계관리 > 거래처관리] [DETAIL] URL 확인",
"action": "verify_elements", "action": "verify_url",
"checks": [ "target": "/accounting/vendors/",
"메모 카드 존재", "expected": "URL에 거래처 ID 포함"
"메모 리스트 또는 빈 메시지"
],
"expected": "메모 섹션 표시"
}, },
{ {
"id": 21, "id": 21,
"name": "핵심 테스트: 수정 버튼 클릭", "name": "[회계관리 > 거래처관리] [DETAIL] 전체 섹션 필드 수 검증 (6개 섹션)",
"action": "click_if_exists", "action": "evaluate",
"target": "수정", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SECTION_VERIFY'};const pageText=document.body.innerText;const sectionChecks=[{name:'기본정보',keywords:['사업자등록번호','거래처코드','거래처명','대표자명']},{name:'연락처',keywords:['주소','전화번호','이메일']},{name:'담당자',keywords:['담당자','담당자명']},{name:'회사정보',keywords:['매입 결제일','매출 결제일']},{name:'신용/거래',keywords:['신용등급','거래등급','계좌']},{name:'추가정보',keywords:['미수금','악성채권']}];let foundSections=0;R.sections={};sectionChecks.forEach(sec=>{const fieldsFound=sec.keywords.filter(kw=>pageText.includes(kw));const pct=Math.round(fieldsFound.length/sec.keywords.length*100);R.sections[sec.name]={total:sec.keywords.length,found:fieldsFound.length,pct:pct+'%',fields:fieldsFound};if(fieldsFound.length>0)foundSections++;});R.foundSections=foundSections;R.totalSections=sectionChecks.length;R.ok=foundSections>=4;R.info=foundSections+'/'+sectionChecks.length+' 섹션 확인';return JSON.stringify(R);})()",
"expected": "수정 모드로 전환 (URL에 ?mode=edit 추가)" "timeout": 15000,
"phase": "VERIFY"
}, },
{ {
"id": 22, "id": 22,
"name": "수정 모드 - URL 확인", "name": "[회계관리 > 거래처관리] [DETAIL] 상세 필드 1:1 대조 (목록 캡처 vs 상세)",
"action": "verify_url", "action": "evaluate",
"target": "mode=edit", "script": "(async()=>{const R={phase:'DETAIL_MATCH'};const cap=window.__E2E_CAPTURED__||{};R.hasCaptured=Object.keys(cap).length>0;if(!R.hasCaptured){R.ok=true;R.info='캡처 데이터 없음 - 스킵';return JSON.stringify(R);}const norm=s=>(s||'').replace(/[,\\s]/g,'').trim();const pageText=document.body.innerText;const inputs=Array.from(document.querySelectorAll('input,textarea,select')).filter(i=>i.offsetParent!==null);const allValues=[...inputs.map(i=>i.value)];const matches={};const checks=['vendorName','type','creditGrade','tradeGrade','receivable'];checks.forEach(key=>{const val=cap[key];if(!val||val==='')return;const nv=norm(val);const found=pageText.includes(val)||pageText.includes(nv)||norm(pageText).includes(nv)||allValues.some(v=>v?.includes(val)||norm(v)===nv);matches[key]={expected:val,found};});R.matches=matches;const matchCount=Object.values(matches).filter(m=>m.found).length;const totalChecks=Object.keys(matches).length;R.matchCount=matchCount;R.totalChecks=totalChecks;R.matchRate=totalChecks>0?Math.round(matchCount/totalChecks*100)+'%':'N/A';R.ok=matchCount>=Math.max(1,Math.ceil(totalChecks*0.4));return JSON.stringify(R);})()",
"checks": [ "timeout": 15000,
"URL에 mode=edit 파라미터 포함" "phase": "VERIFY"
],
"expected": "수정 모드 URL 정상"
}, },
{ {
"id": 23, "id": 23,
"name": "수정 모드 - 필드 편집 가능 확인", "name": "[회계관리 > 거래처관리] [DETAIL] 헤더 버튼 확인 (목록/수정/삭제)",
"action": "verify_edit_mode", "action": "evaluate",
"checks": [ "script": "(()=>{const R={phase:'HEADER_BTNS'};const btns=Array.from(document.querySelectorAll('button')).filter(b=>b.offsetParent!==null);R.listBtn=btns.some(b=>b.innerText?.trim()==='목록');R.editBtn=btns.some(b=>b.innerText?.trim()==='수정');R.deleteBtn=btns.some(b=>b.innerText?.trim()==='삭제');R.ok=R.editBtn;return JSON.stringify(R);})()",
"거래처명 입력 가능", "timeout": 10000,
"대표자명 입력 가능", "phase": "VERIFY"
"전화번호 입력 가능",
"이메일 입력 가능",
"저장 버튼 존재",
"취소 버튼 존재"
],
"expected": "수정 모드에서 필드 편집 가능"
}, },
{ {
"id": 24, "id": 24,
"name": "핵심 테스트: 거래처명 수정", "name": "[회계관리 > 거래처관리] [EDIT] 수정 버튼 클릭",
"description": "거래처명 input 필드에 테스트 접미사 추가. input에 id/name이 없으므로 value 기반 탐색 필요",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const inputs = document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])'); let target = null; inputs.forEach(inp => { if(inp.value && inp.value.length > 1 && !inp.value.includes('수정테스트') && !inp.placeholder.includes('자동생성') && !inp.placeholder.includes('000-00')) { if(!target) target = inp; } }); if(!target) { for(const inp of inputs) { if(inp.value && inp.value.length > 1 && !inp.placeholder.includes('자동생성') && !inp.placeholder.includes('000-00')) { target = inp; break; } } } if(target) { const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const origVal = target.value; nset.call(target, origVal + ' (수정테스트)'); target.dispatchEvent(new Event('input',{bubbles:true})); target.dispatchEvent(new Event('change',{bubbles:true})); window.__e2e_origVendorName = origVal; return 'modified: ' + origVal + ' → ' + target.value; } return 'no editable input found'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'EDIT_ENTER'};const btn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정'&&b.offsetParent!==null);if(btn){btn.click();await w(2000);R.editUrl=location.pathname+location.search;R.isEditMode=location.search.includes('mode=edit');}else{R.warn='수정 버튼 없음';}R.ok=true;return JSON.stringify(R);})()",
"expected": "거래처명에 ' (수정테스트)' 추가" "timeout": 15000,
"phase": "UPDATE"
}, },
{ {
"id": 25, "id": 25,
"name": "핵심 테스트: 저장 버튼 클릭", "name": "[회계관리 > 거래처관리] [EDIT] 수정 모드 URL 확인",
"description": "저장 버튼 클릭. 이 페이지는 다이얼로그 없이 직접 저장 후 목록으로 리다이렉트됨", "action": "verify_url",
"action": "evaluate", "target": "mode=edit",
"script": "(async () => { window.__e2e_urlBeforeSave = window.location.href; const saveBtn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'); if(saveBtn){ saveBtn.click(); await new Promise(r=>setTimeout(r,2000)); return 'saved, url=' + window.location.href; } return 'save button not found'; })()", "expected": "수정 모드 URL 정상"
"expected": "저장 완료 후 목록 페이지로 리다이렉트"
}, },
{ {
"id": 26, "id": 26,
"name": "필수 검증 #2: 저장 완료 확인", "name": "[회계관리 > 거래처관리] [EDIT] 다중 필드 원본 값 캡처 + 수정",
"description": "저장 후 URL 변경 및 에러 여부 확인 (다이얼로그 없이 직접 저장 방식)",
"action": "evaluate", "action": "evaluate",
"script": "(() => { const url = window.location.href; const isListPage = url.includes('/accounting/vendors') && !url.includes('mode='); const hasError = document.body.innerText.includes('404') || document.body.innerText.includes('500') || document.body.innerText.includes('Not Found'); const urlChanged = url !== window.__e2e_urlBeforeSave; return JSON.stringify({ url, isListPage, hasError, urlChanged, result: isListPage && !hasError ? 'PASS' : 'FAIL' }); })()", "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:'MULTI_EDIT'};const inputs=Array.from(document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);R.editableCount=inputs.length;window.__E2E_ORIG_VALUES__={};let edited=0;for(const inp of inputs){const val=inp.value||'';const ph=inp.placeholder||'';if(val.includes('수정테스트'))continue;if(ph.includes('자동생성')||ph.includes('000-00'))continue;if(val.length>1&&edited===0){window.__E2E_ORIG_VALUES__['field0']={index:Array.from(inputs).indexOf(inp),value:val};sv(inp,val+' (수정테스트)');edited++;R.field0={orig:val,new:inp.value};await w(200);continue;}if(val.length>1&&edited===1){window.__E2E_ORIG_VALUES__['field1']={index:Array.from(inputs).indexOf(inp),value:val};sv(inp,val+' (E2E수정)');edited++;R.field1={orig:val,new:inp.value};await w(200);continue;}if(edited>=2)break;}R.editedFields=edited;R.ok=edited>0;return JSON.stringify(R);})()",
"expected": "목록 페이지로 복귀, 에러 없음" "timeout": 15000,
"phase": "UPDATE"
}, },
{ {
"id": 27, "id": 27,
"name": "수정 결과 확인 - 목록에서 검증", "name": "[회계관리 > 거래처관리] [EDIT] 저장 클릭",
"description": "목록 페이지에서 수정된 거래처명 확인",
"action": "evaluate", "action": "evaluate",
"script": "(() => { const found = document.body.innerText.includes('수정테스트'); const rows = document.querySelectorAll('table tbody tr').length; return JSON.stringify({ modifiedVisible: found, rowCount: rows, result: found ? 'PASS: 수정된 데이터 목록에 반영' : 'WARN: 수정 텍스트 미표시 (페이지네이션 또는 정렬 영향)' }); })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SAVE'};window.__e2e_urlBeforeSave=window.location.href;const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'&&b.offsetParent!==null);if(saveBtn){saveBtn.click();await w(3000);R.urlAfter=location.href;R.saved=true;}else{R.error='저장 버튼 없음';}R.ok=!!saveBtn;return JSON.stringify(R);})()",
"expected": "수정된 거래처명이 목록에 표시" "timeout": 20000,
"phase": "UPDATE"
}, },
{ {
"id": 28, "id": 28,
"name": "원래 값 복원 - 수정된 거래처 클릭", "name": "[회계관리 > 거래처관리] [EDIT] 저장 완료 확인 (목록 복귀 + 에러 없음)",
"description": "수정된 거래처를 찾아 클릭하여 상세 페이지 진입",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const rows = document.querySelectorAll('table tbody tr'); let target = null; rows.forEach(row => { if(row.innerText.includes('수정테스트')) target = row; }); if(target){ target.click(); await new Promise(r=>setTimeout(r,2000)); return 'clicked modified vendor, url=' + window.location.href; } rows[0]?.click(); await new Promise(r=>setTimeout(r,2000)); return 'modified vendor not found in current page, clicked first row. url=' + window.location.href; })()", "script": "(()=>{const R={phase:'SAVE_VERIFY'};const url=window.location.href;R.isListPage=url.includes('/accounting/vendors')&&!url.includes('mode=');const hasError=document.body.innerText.includes('404')||document.body.innerText.includes('500')||document.body.innerText.includes('Not Found');R.hasError=hasError;R.urlChanged=url!==window.__e2e_urlBeforeSave;R.ok=R.isListPage&&!hasError;R.info=R.ok?'PASS: 목록 복귀, 에러 없음':'FAIL: isListPage='+R.isListPage+', hasError='+hasError;return JSON.stringify(R);})()",
"expected": "수정된 거래처 상세 페이지 진입" "timeout": 10000,
"phase": "VERIFY"
}, },
{ {
"id": 29, "id": 29,
"name": "원래 값 복원 - 수정 버튼 클릭", "name": "[회계관리 > 거래처관리] [EDIT] 목록에서 수정 반영 확인",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정'); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,1500)); return 'edit mode, url=' + window.location.href; } return 'edit button not found'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'VERIFY_EDIT'};const found=document.body.innerText.includes('수정테스트');const foundE2E=document.body.innerText.includes('E2E수정');R.modifiedVisible=found;R.secondFieldVisible=foundE2E;R.rowCount=document.querySelectorAll('table tbody tr').length;R.ok=true;R.info=found?'PASS: 수정된 데이터 목록에 반영':'WARN: 수정 텍스트 미표시 (페이지네이션 또는 정렬 영향)';return JSON.stringify(R);})()",
"expected": "수정 모드로 전환" "timeout": 10000,
"phase": "VERIFY"
}, },
{ {
"id": 30, "id": 30,
"name": "원래 값 복원 - 거래처명 원복", "name": "[회계관리 > 거래처관리] [VERIFY-EDIT] 수정된 거래처 재진입하여 저장 검증",
"description": "거래처명에서 ' (수정테스트)' 제거",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const inputs = document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])'); let restored = false; inputs.forEach(inp => { if(inp.value.includes('수정테스트')){ const nset = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const newVal = inp.value.replace(' (수정테스트)',''); nset.call(inp, newVal); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); restored = true; } }); return restored ? 'restored' : 'no field with 수정테스트 found'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'RE_ENTER_DETAIL'};const rows=Array.from(document.querySelectorAll('table tbody tr'));let target=rows.find(row=>row.innerText?.includes('수정테스트'));if(!target)target=rows[0];if(target){target.click();await w(2500);const pageText=document.body.innerText;R.has수정테스트=pageText.includes('수정테스트');R.hasE2E수정=pageText.includes('E2E수정');R.detailUrl=location.pathname+location.search;}R.ok=true;return JSON.stringify(R);})()",
"expected": "거래처명에서 ' (수정테스트)' 제거" "timeout": 15000,
"phase": "VERIFY"
}, },
{ {
"id": 31, "id": 31,
"name": "원래 값 복원 - 저장", "name": "[회계관리 > 거래처관리] [RESTORE] 수정 모드 진입",
"description": "복원 저장 (다이얼로그 없이 직접 저장)",
"action": "evaluate", "action": "evaluate",
"script": "(async () => { const btn = Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'); if(btn){ btn.click(); await new Promise(r=>setTimeout(r,2000)); return 'saved, url=' + window.location.href; } return 'save button not found'; })()", "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'RESTORE_EDIT'};const btn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='수정'&&b.offsetParent!==null);if(btn){btn.click();await w(1500);R.editUrl=location.pathname+location.search;}else{R.warn='수정 버튼 없음';}R.ok=true;return JSON.stringify(R);})()",
"expected": "원래 값으로 복원 완료, 목록으로 리다이렉트" "timeout": 15000,
"phase": "RESTORE"
}, },
{ {
"id": 32, "id": 32,
"name": "원래 값 복원 - 완료 확인", "name": "[회계관리 > 거래처관리] [RESTORE] 원래 값 복원 (다중 필드)",
"description": "복원 후 목록 페이지에서 수정테스트 텍스트 제거 확인",
"action": "evaluate", "action": "evaluate",
"script": "(() => { const url = window.location.href; const isListPage = url.includes('/accounting/vendors') && !url.includes('mode='); const stillModified = document.body.innerText.includes('수정테스트'); return JSON.stringify({ url, isListPage, stillModified, result: isListPage && !stillModified ? 'PASS: 원복 완료' : 'WARN: 원복 확인 필요' }); })()", "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:'RESTORE_VALUES'};const inputs=Array.from(document.querySelectorAll('input:not([type=\"hidden\"]):not([type=\"checkbox\"])')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);let restored=0;inputs.forEach(inp=>{if(inp.value.includes(' (수정테스트)')){const newVal=inp.value.replace(' (수정테스트)','');sv(inp,newVal);restored++;R.field0Restored=true;}if(inp.value.includes(' (E2E수정)')){const newVal=inp.value.replace(' (E2E수정)','');sv(inp,newVal);restored++;R.field1Restored=true;}});R.restoredCount=restored;R.ok=restored>0;R.info=restored>0?restored+' fields restored':'no fields with test markers found';return JSON.stringify(R);})()",
"expected": "수정테스트 텍스트 제거됨" "timeout": 15000,
"phase": "RESTORE"
}, },
{ {
"id": 33, "id": 33,
"name": "목록 페이지 최종 확인", "name": "[회계관리 > 거래처관리] [RESTORE] 저장 클릭",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'RESTORE_SAVE'};const btn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'&&b.offsetParent!==null);if(btn){btn.click();await w(3000);R.url=location.href;R.saved=true;}else{R.error='저장 버튼 없음';}R.ok=!!btn;return JSON.stringify(R);})()",
"timeout": 20000,
"phase": "RESTORE"
},
{
"id": 34,
"name": "[회계관리 > 거래처관리] [RESTORE] 복원 완료 확인",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));await w(1000);const R={phase:'RESTORE_VERIFY'};const url=window.location.href;R.isListPage=url.includes('/accounting/vendors')&&!url.includes('mode=');const still수정=document.body.innerText.includes('수정테스트');const stillE2E=document.body.innerText.includes('E2E수정');R.still수정=still수정;R.stillE2E=stillE2E;R.ok=R.isListPage&&!still수정;R.info=(!still수정&&!stillE2E)?'PASS: 원복 완료':'WARN: 원복 확인 필요 (수정테스트='+still수정+', E2E수정='+stillE2E+')';return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 35,
"name": "[회계관리 > 거래처관리] [FINAL] 목록 페이지 최종 확인",
"action": "verify_url", "action": "verify_url",
"target": "/accounting/vendors", "target": "/accounting/vendors",
"expected": "거래처관리 목록 페이지 정상 표시" "expected": "거래처관리 목록 페이지 정상 표시"
}, },
{ {
"id": 34, "id": 36,
"name": "콘솔 에러 확인", "name": "[회계관리 > 거래처관리] [FINAL] API 요약 + 콘솔 에러 확인",
"action": "verify_element", "action": "evaluate",
"target": "body", "script": "(()=>{const R={phase:'FINAL_SUMMARY'};const logs=window.__API_LOGS__||[];R.apiSummary={total:logs.length,success:logs.filter(l=>l.ok||l.status<400).length,failed:logs.filter(l=>!l.ok&&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,methods:{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}};const errs=window.__CONSOLE_ERRORS__||[];R.consoleErrors=errs.length;R.errorSamples=errs.slice(0,5);R.ok=true;R.info='API calls: '+logs.length+', errors: '+errs.length;return JSON.stringify(R);})()",
"expected": "심각한 콘솔 에러 없음" "timeout": 10000,
"phase": "VERIFY"
} }
], ],
"requiredVerifications": [ "requiredVerifications": [
{ { "id": 1, "name": "등록/저장 버튼", "steps": [27, 28], "criteria": "저장 클릭 → 목록 리다이렉트 + 에러 없음 + 데이터 반영" },
"id": 1, { "id": 3, "name": "검색/필터", "steps": [9, 10, 11, 12, 13, 14, 15, 16], "criteria": "검색/네거티브/필터/복합필터 시 데이터 변화 확인" },
"name": "등록/저장 버튼", { "id": 5, "name": "목업 페이지 감지", "steps": [6], "criteria": "입력 필드, 동작 버튼, API 호출 확인" }
"steps": [
25,
26
],
"criteria": "저장 클릭 → 목록 리다이렉트 + 에러 없음 + 데이터 반영"
},
{
"id": 2,
"name": "등록 버튼 (신규)",
"steps": [],
"criteria": "보류 - 추후 구현 예정"
},
{
"id": 3,
"name": "검색/필터",
"steps": [
6,
7,
8,
9,
10
],
"criteria": "검색 및 필터 시 데이터 변화 확인"
},
{
"id": 4,
"name": "삭제 기능",
"steps": [],
"criteria": "보류 - 테스트 대상에서 제외"
},
{
"id": 5,
"name": "목업 페이지 감지",
"steps": [
3
],
"criteria": "입력 필드, 동작 버튼, API 호출 확인"
}
], ],
"testData": { "testData": {
"searchKeyword": "가우스", "searchKeyword": "가우스",
"editSuffix": " (수정테스트)" "negativeKeyword": "ZZZZNONEXIST99",
"editSuffix": " (수정테스트)",
"editSuffix2": " (E2E수정)"
}, },
"expectedAPIs": [ "expectedAPIs": [
{ { "method": "GET", "endpoint": "/api/v1/clients", "description": "거래처 목록 조회" },
"method": "GET", { "method": "GET", "endpoint": "/api/v1/clients/{id}", "description": "거래처 상세 조회" },
"endpoint": "/api/v1/clients", { "method": "PUT", "endpoint": "/api/v1/clients/{id}", "description": "거래처 수정" }
"description": "거래처 목록 조회"
},
{
"method": "GET",
"endpoint": "/api/v1/clients/{id}",
"description": "거래처 상세 조회"
},
{
"method": "PUT",
"endpoint": "/api/v1/clients/{id}",
"description": "거래처 수정"
}
] ]
} }