Files
sam-scenarios/production-work-order.json

291 lines
29 KiB
JSON
Raw Normal View History

{
"enabled": true,
"id": "production-work-order",
"name": "작업지시 CRUD + 필드검증 + API확인: 생산관리",
"version": "2.0.0",
"screenshotPolicy": {
"captureOnFail": true,
"captureOnPass": false
},
"description": "생산관리 > 작업지시 관리 메뉴의 작업지시 CRUD 전체 흐름 + combobox/date picker + 상세 필드 대조 + API 검증",
"baseUrl": "https://dev.codebridge-x.com",
"menuNavigation": {
"level1": "생산관리",
"level2": "작업지시 관리",
"expectedUrl": "/production/work-orders",
"searchWithinParent": true,
"closeOtherMenus": true
},
"auth": {
"role": "admin"
},
"testData": {
"create": {
"orderNumber": "E2E_TEST_작업지시",
"quantity": "500",
"memo": "E2E 자동화 테스트 작업지시"
},
"update": {
"quantity": "600",
"memo": "E2E 수정된 작업지시 메모"
}
},
"steps": [
{
"id": 1,
"name": "[생산관리 > 작업지시 관리] 페이지 로드 대기",
"action": "wait",
"timeout": 5000
},
{
"id": 2,
"name": "[생산관리 > 작업지시 관리] ts 초기화 + API 모니터링",
"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": 3,
"name": "[생산관리 > 작업지시 관리] 테이블 로드 대기",
"action": "wait_for_table",
"timeout": 20000
},
{
"id": 4,
"name": "[생산관리 > 작업지시 관리] 목업 페이지 감지",
"action": "verify_not_mockup",
"checks": [
"작업지시 목록 표시",
"작업지시 등록 버튼 존재",
"검색/필터 기능 존재"
],
"expected": "정상 페이지 (목업 아님)"
},
{
"id": 5,
"name": "[생산관리 > 작업지시 관리] 테이블 구조 확인",
"action": "verify_table",
"checks": [
"작업지시번호 컬럼",
"품목 컬럼",
"수량 컬럼",
"납기일 컬럼",
"상태 컬럼"
],
"expected": "작업지시 테이블 컬럼 정상 표시"
},
{
"id": 6,
"name": "[생산관리 > 작업지시 관리] [CREATE] 등록 버튼 클릭",
"action": "evaluate",
"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,
"name": "[생산관리 > 작업지시 관리] [CREATE] 등록 폼 로드 대기",
"action": "wait",
"timeout": 2000
},
{
"id": 8,
"name": "[생산관리 > 작업지시 관리] [CREATE] ts 생성 + 작업지시번호/메모 입력",
"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__||(()=>{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);})()",
"timeout": 15000,
"phase": "CREATE"
},
{
"id": 9,
"name": "[생산관리 > 작업지시 관리] [CREATE] 품목 combobox 선택",
"action": "evaluate",
"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);})()",
"timeout": 15000,
"phase": "CREATE"
},
{
"id": 10,
"name": "[생산관리 > 작업지시 관리] [CREATE] 수량 입력 (500)",
"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 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);})()",
"timeout": 10000,
"phase": "CREATE"
},
{
"id": 11,
"name": "[생산관리 > 작업지시 관리] [CREATE] 납기일 date picker 선택",
"action": "evaluate",
"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);})()",
"timeout": 15000,
"phase": "CREATE"
},
{
"id": 12,
"name": "[생산관리 > 작업지시 관리] [CREATE] 기타 combobox 선택 (우선순위 등)",
"action": "evaluate",
"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);})()",
"timeout": 20000,
"phase": "CREATE"
},
{
"id": 13,
"name": "[생산관리 > 작업지시 관리] [CREATE] 등록 클릭",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SUBMIT'};const sub=Array.from(document.querySelectorAll('button')).find(b=>/^등록$|^저장$|^확인$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!sub){R.error='등록/저장 버튼 없음';R.ok=false;return JSON.stringify(R);}sub.click();await w(3000);const 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);})()",
"timeout": 20000,
"phase": "CREATE"
},
{
"id": 14,
"name": "[생산관리 > 작업지시 관리] [CREATE] API POST 검증",
"action": "evaluate",
"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);})()",
"timeout": 10000,
"phase": "VERIFY"
},
{
"id": 15,
"name": "[생산관리 > 작업지시 관리] [CREATE] 모달 닫기 + 목록 복귀",
"action": "evaluate",
"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);})()",
"timeout": 15000,
"phase": "CREATE"
},
{
"id": 16,
"name": "[생산관리 > 작업지시 관리] [CREATE] 목록 안정화 대기",
"action": "wait",
"timeout": 2000
},
{
"id": 17,
"name": "[생산관리 > 작업지시 관리] [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_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);})()",
"timeout": 20000,
"phase": "VERIFY"
},
{
"id": 18,
"name": "[생산관리 > 작업지시 관리] [READ] E2E 행 클릭 → 상세 진입",
"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];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);})()",
"timeout": 15000,
"phase": "READ"
},
{
"id": 19,
"name": "[생산관리 > 작업지시 관리] [READ] 상세 페이지 로드 대기",
"action": "wait",
"timeout": 2000
},
{
"id": 20,
"name": "[생산관리 > 작업지시 관리] [READ] 상세 필드 1:1 대조 검증",
"action": "evaluate",
"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);})()",
"timeout": 15000,
"phase": "VERIFY"
},
{
"id": 21,
"name": "[생산관리 > 작업지시 관리] [UPDATE] 수정 모드 진입",
"action": "evaluate",
"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);})()",
"timeout": 15000,
"phase": "UPDATE"
},
{
"id": 22,
"name": "[생산관리 > 작업지시 관리] [UPDATE] 수량 500→600 수정",
"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 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);})()",
"timeout": 10000,
"phase": "UPDATE"
},
{
"id": 23,
"name": "[생산관리 > 작업지시 관리] [UPDATE] 메모 수정",
"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:'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);})()",
"timeout": 10000,
"phase": "UPDATE"
},
{
"id": 24,
"name": "[생산관리 > 작업지시 관리] [UPDATE] 저장 클릭",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'UPDATE_SAVE'};const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>/^저장$|^수정$|^확인$/.test(b.innerText?.trim())&&b.offsetParent!==null&&!b.disabled);if(!saveBtn){R.error='저장 버튼 없음';R.ok=false;return JSON.stringify(R);}saveBtn.click();await w(3000);const 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);})()",
"timeout": 20000,
"phase": "UPDATE"
},
{
"id": 25,
"name": "[생산관리 > 작업지시 관리] [UPDATE] API PUT 검증",
"action": "evaluate",
"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": [
{ "method": "GET", "endpoint": "/api/v1/work-orders", "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": [
{ "id": 2, "name": "등록/저장 버튼", "steps": [13, 14], "criteria": "API POST + 토스트 + 목록 반영" },
{ "id": 5, "name": "목업 페이지 감지", "steps": [4], "criteria": "작업지시 목록, 등록 버튼, 필터 존재" },
{ "id": 6, "name": "삭제 기능", "steps": [27, 28, 29], "criteria": "DELETE API + 목록에서 제거" }
],
"rollbackPlan": {
"onCreateFail": "모달 닫기",
"onUpdateFail": "테스트 작업지시 수동 삭제 필요",
"onDeleteFail": "테스트 작업지시 수동 삭제 필요",
"cleanupRequired": "E2E_TEST_ 접두사 작업지시는 테스트 데이터"
}
}