From 9a4a9ed5dba5ec760e6591ffc3aa26ac81b1eb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 13 Feb 2026 20:02:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A7=A4=EC=B6=9C=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=A7=91=EC=A4=91=20=EC=A0=95=EB=B0=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=208=EC=A2=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(126=EC=8A=A4=ED=85=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- batch-update-account-sales.json | 120 +++++++++++++++++++++ detail-verify-acc-sales.json | 106 ++++++++++++++++++ edge-boundary-acc-sales.json | 122 +++++++++++++++++++++ edge-rapid-click-acc-sales.json | 90 ++++++++++++++++ full-crud-acc-sales.json | 163 ++++++++++++++++++++++++++++ multi-item-acc-sales.json | 185 ++++++++++++++++++++++++++++++++ reload-persist-acc-sales.json | 131 ++++++++++++++++++++++ search-filter-acc-sales.json | 152 ++++++++++++++++++++++++++ 8 files changed, 1069 insertions(+) create mode 100644 batch-update-account-sales.json create mode 100644 detail-verify-acc-sales.json create mode 100644 edge-boundary-acc-sales.json create mode 100644 edge-rapid-click-acc-sales.json create mode 100644 full-crud-acc-sales.json create mode 100644 multi-item-acc-sales.json create mode 100644 reload-persist-acc-sales.json create mode 100644 search-filter-acc-sales.json diff --git a/batch-update-account-sales.json b/batch-update-account-sales.json new file mode 100644 index 0000000..2d459ce --- /dev/null +++ b/batch-update-account-sales.json @@ -0,0 +1,120 @@ +{ + "id": "batch-update-account-sales", + "name": "계정과목 일괄변경 버그 회귀 테스트 (BUG-SALES-20260115-001): 매출관리", + "version": "1.0.0", + "auth": { + "role": "admin" + }, + "menuNavigation": { + "level1": "회계관리", + "level2": "매출관리" + }, + "screenshotPolicy": { + "captureOnFail": true, + "captureOnPass": false + }, + "steps": [ + { + "id": 1, + "name": "[회계관리 > 매출관리] 페이지 로드 대기", + "action": "wait", + "timeout": 3000 + }, + { + "id": 2, + "name": "[회계관리 > 매출관리] 테이블 로드 대기", + "action": "wait_for_table", + "timeout": 8000 + }, + { + "id": 3, + "name": "[회계관리 > 매출관리] [CAPTURE] 첫 행 현재 매출유형 캡처", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CAPTURE_BEFORE'};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');R.cellCount=cells.length;const typeColIdx=[5,4,3].find(idx=>cells[idx]&&cells[idx].innerText?.trim()!=='');const typeValue=typeColIdx!==undefined?cells[typeColIdx]?.innerText?.trim():'';R.beforeType=typeValue;window.__E2E_BEFORE_TYPE__=typeValue;R.firstRowText=firstRow.innerText?.substring(0,100);R.ok=true;return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "CAPTURE" + }, + { + "id": 4, + "name": "[회계관리 > 매출관리] [SELECT] 첫 행 체크박스 선택", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SELECT_ROW'};const rows=document.querySelectorAll('table tbody tr');if(rows.length===0){R.error='행 없음';R.ok=false;return JSON.stringify(R);}const firstRow=rows[0];const checkbox=firstRow.querySelector('input[type=\"checkbox\"],button[role=\"checkbox\"]');if(checkbox){checkbox.click();await w(500);R.checked=true;}else{firstRow.querySelector('td')?.click();await w(500);R.clickedFirstCell=true;}R.ok=true;return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "SELECT" + }, + { + "id": 5, + "name": "[회계관리 > 매출관리] [SELECT] 계정과목 드롭다운에서 다른 값 선택", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CHANGE_ACCOUNT'};const beforeType=window.__E2E_BEFORE_TYPE__||'';const combos=Array.from(document.querySelectorAll('button[role=\"combobox\"],select')).filter(b=>b.offsetParent!==null);const labels=Array.from(document.querySelectorAll('label')).filter(l=>/계정과목|매출유형/.test(l.innerText));R.comboCount=combos.length;R.labelCount=labels.length;let targetCombo=null;for(const cb of combos){const lbl=cb.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||'';if(lbl.includes('계정과목')||lbl.includes('매출유형')){targetCombo=cb;break;}}if(!targetCombo&&combos.length>0){targetCombo=combos[0];}if(!targetCombo){const selectEls=document.querySelectorAll('select');for(const sel of selectEls){const lbl=sel.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||'';if(lbl.includes('계정과목')){targetCombo=sel;break;}}if(!targetCombo&&selectEls.length>0)targetCombo=selectEls[0];}if(!targetCombo){R.error='계정과목 콤보박스 없음';R.ok=false;return JSON.stringify(R);}if(targetCombo.tagName==='SELECT'){const opts=targetCombo.options;for(let i=0;io.innerText?.trim()!==beforeType&&o.innerText?.trim()!==''&&o.innerText?.trim()!=='미설정');if(diff){diff.click();R.selectedValue=diff.innerText?.trim();await w(400);}else if(opts[0]){opts[0].click();R.selectedValue=opts[0].innerText?.trim();await w(400);}}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}R.ok=true;}window.__E2E_NEW_TYPE__=R.selectedValue||'';return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "SELECT" + }, + { + "id": 6, + "name": "[회계관리 > 매출관리] [SAVE] 저장 버튼 클릭", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SAVE'};const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'&&b.offsetParent!==null&&!b.disabled);if(!saveBtn){R.error='저장 버튼 없음';R.ok=false;return JSON.stringify(R);}saveBtn.click();await w(1500);R.ok=true;return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "SAVE" + }, + { + "id": 7, + "name": "[회계관리 > 매출관리] [SAVE] 확인 다이얼로그 → 확인 클릭", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const toastInfo=()=>{const t=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');return{count:t.length,text:t.length>0?Array.from(t).pop()?.innerText?.trim().substring(0,100):''};};const R={phase:'CONFIRM'};const dlg=document.querySelector('[role=\"alertdialog\"],[role=\"dialog\"]');if(dlg&&dlg.offsetParent!==null){R.dialogText=dlg.innerText?.substring(0,100);const cfmBtn=Array.from(dlg.querySelectorAll('button')).find(b=>/확인|저장|예|Yes/.test(b.innerText?.trim()));if(cfmBtn){cfmBtn.click();await w(3000);}R.dialogConfirmed=true;}else{R.dialogFound=false;await w(2000);}R.toast=toastInfo();R.ok=true;return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "SAVE" + }, + { + "id": 8, + "name": "[회계관리 > 매출관리] [VERIFY-1] 토스트 메시지 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'VERIFY_TOAST'};await w(1000);const toasts=document.querySelectorAll('[data-sonner-toast],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"Toaster\"] [data-content]');R.toastCount=toasts.length;R.toastTexts=Array.from(toasts).map(t=>t.innerText?.trim().substring(0,80)).filter(Boolean);const successToast=R.toastTexts.some(t=>/성공|완료|저장|변경/.test(t));R.successToast=successToast;R.ok=true;R.info=successToast?'✅ 성공 토스트 확인':'⚠️ 성공 토스트 미감지';return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "VERIFY" + }, + { + "id": 9, + "name": "[회계관리 > 매출관리] [VERIFY-2] ★핵심★ 첫 행 매출유형 실제 변경 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'VERIFY_DATA_CHANGED'};await w(1000);const beforeType=window.__E2E_BEFORE_TYPE__||'';const newType=window.__E2E_NEW_TYPE__||'';const rows=document.querySelectorAll('table tbody tr');if(rows.length===0){R.error='테이블 행 없음';R.ok=false;return JSON.stringify(R);}const firstRow=rows[0];const cells=firstRow.querySelectorAll('td');const currentTypes=Array.from(cells).map(c=>c.innerText?.trim()).filter(Boolean);R.beforeType=beforeType;R.expectedNewType=newType;R.currentCellValues=currentTypes.slice(0,8);const changed=newType&¤tTypes.some(v=>v.includes(newType));const unchanged=beforeType&¤tTypes.some(v=>v===beforeType);R.dataActuallyChanged=changed;R.dataStillOld=unchanged&&!changed;R.ok=true;if(R.dataStillOld&&beforeType!==newType){R.bugDetected=true;R.info='🐛 BUG-SALES-20260115-001 재현: 토스트 성공 but 데이터 미변경 (beforeType='+beforeType+', expected='+newType+')';}else if(R.dataActuallyChanged){R.info='✅ 데이터 실제 변경 확인 ('+beforeType+' → '+newType+')';}else{R.info='⚠️ 변경 확인 불가 (beforeType='+beforeType+', newType='+newType+')';}return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "VERIFY" + }, + { + "id": 10, + "name": "[회계관리 > 매출관리] [RELOAD] 새로고침", + "action": "reload", + "timeout": 10000 + }, + { + "id": 11, + "name": "[회계관리 > 매출관리] [RELOAD] 새로고침 후 대기", + "action": "wait", + "timeout": 5000 + }, + { + "id": 12, + "name": "[회계관리 > 매출관리] [RELOAD] 테이블 재로드 대기", + "action": "wait_for_table", + "timeout": 8000 + }, + { + "id": 13, + "name": "[회계관리 > 매출관리] [VERIFY-3] ★핵심★ 새로고침 후에도 변경값 유지 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'VERIFY_PERSIST'};await w(1000);const beforeType=window.__E2E_BEFORE_TYPE__||'';const newType=window.__E2E_NEW_TYPE__||'';const rows=document.querySelectorAll('table tbody tr');if(rows.length===0){R.error='테이블 행 없음 after reload';R.ok=false;return JSON.stringify(R);}const firstRow=rows[0];const cells=firstRow.querySelectorAll('td');const currentTypes=Array.from(cells).map(c=>c.innerText?.trim()).filter(Boolean);R.beforeType=beforeType;R.expectedNewType=newType;R.currentCellValues=currentTypes.slice(0,8);const persisted=newType&¤tTypes.some(v=>v.includes(newType));const reverted=beforeType&¤tTypes.some(v=>v===beforeType)&&!persisted;R.persistedAfterReload=persisted;R.revertedAfterReload=reverted;R.ok=true;if(reverted&&beforeType!==newType){R.bugDetected=true;R.info='🐛 BUG-SALES-20260115-001 재현: 새로고침 후 원래값 복귀 ('+beforeType+')';}else if(persisted){R.info='✅ 새로고침 후에도 변경값 유지 ('+newType+')';}else{R.info='⚠️ 새로고침 후 상태 확인 불가';}return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "VERIFY" + }, + { + "id": 14, + "name": "[회계관리 > 매출관리] [RESTORE] 원래 값 복원 (선택적)", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'RESTORE'};const beforeType=window.__E2E_BEFORE_TYPE__||'';if(!beforeType){R.info='원래 값 없음 - 복원 스킵';R.ok=true;return JSON.stringify(R);}const rows=document.querySelectorAll('table tbody tr');if(rows.length===0){R.ok=true;return JSON.stringify(R);}const firstRow=rows[0];const checkbox=firstRow.querySelector('input[type=\"checkbox\"],button[role=\"checkbox\"]');if(checkbox)checkbox.click();await w(500);const combos=Array.from(document.querySelectorAll('button[role=\"combobox\"],select')).filter(b=>b.offsetParent!==null);if(combos.length>0){const cb=combos[0];if(cb.tagName==='SELECT'){for(let i=0;io.innerText?.trim()===beforeType);if(opt){opt.click();await w(400);}else{document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(200);}}}}const saveBtn=Array.from(document.querySelectorAll('button')).find(b=>b.innerText?.trim()==='저장'&&b.offsetParent!==null);if(saveBtn){saveBtn.click();await w(1500);const cfm=Array.from(document.querySelectorAll('[role=\"alertdialog\"] button,[role=\"dialog\"] button')).find(b=>/확인/.test(b.innerText?.trim()));if(cfm){cfm.click();await w(2000);}}R.info='원래 값 복원 시도 완료';R.ok=true;return JSON.stringify(R);})()", + "timeout": 20000, + "phase": "RESTORE" + } + ] +} \ No newline at end of file diff --git a/detail-verify-acc-sales.json b/detail-verify-acc-sales.json new file mode 100644 index 0000000..b089461 --- /dev/null +++ b/detail-verify-acc-sales.json @@ -0,0 +1,106 @@ +{ + "id": "detail-verify-acc-sales", + "name": "목록↔상세 필드별 대조 검증: 매출관리", + "version": "1.0.0", + "auth": { + "role": "admin" + }, + "menuNavigation": { + "level1": "회계관리", + "level2": "매출관리" + }, + "screenshotPolicy": { + "captureOnFail": true, + "captureOnPass": false + }, + "steps": [ + { + "id": 1, + "name": "[회계관리 > 매출관리] 페이지 로드 대기", + "action": "wait", + "timeout": 3000 + }, + { + "id": 2, + "name": "[회계관리 > 매출관리] 테이블 로드 대기", + "action": "wait_for_table", + "timeout": 8000 + }, + { + "id": 3, + "name": "[회계관리 > 매출관리] [CAPTURE] 첫 행 모든 셀 값 캡처", + "action": "evaluate", + "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');R.cellCount=cells.length;const captured={};const colNames=['checkbox','no','salesNo','vendorName','salesDate','salesType','supplyAmount','vat','totalAmount','taxInvoice','transStatement'];cells.forEach((cell,i)=>{const key=colNames[i]||('col'+i);const text=cell.innerText?.trim()||'';const input=cell.querySelector('input[type=\"checkbox\"]');if(input){captured[key]=input.checked?'checked':'unchecked';}else{captured[key]=text;}});window.__E2E_CAPTURED__=captured;R.captured=captured;R.ok=true;return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "CAPTURE" + }, + { + "id": 4, + "name": "[회계관리 > 매출관리] [READ] 첫 행 클릭 → 상세 진입", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'READ'};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);})()", + "timeout": 15000, + "phase": "READ" + }, + { + "id": 5, + "name": "[회계관리 > 매출관리] [READ] 상세 페이지 로드 대기", + "action": "wait", + "timeout": 2000 + }, + { + "id": 6, + "name": "[회계관리 > 매출관리] [VERIFY] 상세 페이지 필드 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 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=['salesNo','vendorName','salesDate','salesType','supplyAmount','vat','totalAmount'];checks.forEach(key=>{const val=cap[key];if(!val||val==='')return;const cleanVal=val.replace(/,/g,'').trim();const found=pageText.includes(val)||pageText.includes(cleanVal)||allValues.some(v=>v?.includes(val)||v?.includes(cleanVal));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,totalChecks-2);return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "VERIFY" + }, + { + "id": 7, + "name": "[회계관리 > 매출관리] [VERIFY] 세금계산서/거래명세서 Switch 상태 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SWITCH_VERIFY'};const pageText=document.body.innerText;R.hasTaxInvoice=pageText.includes('세금계산서');R.hasTransStatement=pageText.includes('거래명세서');const switches=document.querySelectorAll('button[role=\"switch\"],input[type=\"checkbox\"][role=\"switch\"],[class*=\"switch\"],[class*=\"Switch\"]');R.switchCount=switches.length;R.switchStates=Array.from(switches).map((sw,i)=>{const lbl=sw.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText||('switch_'+i);const checked=sw.getAttribute('aria-checked')==='true'||sw.checked||sw.getAttribute('data-state')==='checked';return{label:lbl.trim().substring(0,20),checked};});R.ok=true;return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "VERIFY" + }, + { + "id": 8, + "name": "[회계관리 > 매출관리] [VERIFY] 수정 모드 진입 가능 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'EDIT_ACCESS'};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": "VERIFY" + }, + { + "id": 9, + "name": "[회계관리 > 매출관리] [CANCEL] 취소 클릭", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CANCEL'};const cancelBtn=Array.from(document.querySelectorAll('button,a')).find(b=>/취소|목록|뒤로/.test(b.innerText?.trim())&&b.offsetParent!==null);if(cancelBtn){cancelBtn.click();await w(2000);}else{history.back();await w(2000);}R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "CANCEL" + }, + { + "id": 10, + "name": "[회계관리 > 매출관리] [CANCEL] 목록 복귀 대기", + "action": "wait", + "timeout": 2000 + }, + { + "id": 11, + "name": "[회계관리 > 매출관리] [VERIFY] 목록 복귀 후 테이블 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'BACK_VERIFY'};await w(500);R.url=location.pathname+location.search;R.isListPage=location.pathname.includes('/accounting/sales')&&!location.search.includes('mode=');const rows=document.querySelectorAll('table tbody tr');R.rowCount=rows.length;R.ok=R.rowCount>0;return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "VERIFY" + }, + { + "id": 12, + "name": "[회계관리 > 매출관리] [VERIFY] 취소 후 데이터 무변경 확인", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'NO_CHANGE_VERIFY'};const cap=window.__E2E_CAPTURED__||{};if(!cap.salesNo){R.ok=true;R.info='캡처 데이터 없어 스킵';return JSON.stringify(R);}await w(500);const rows=document.querySelectorAll('table tbody tr');if(rows.length===0){R.ok=true;R.info='빈 테이블';return JSON.stringify(R);}const firstRow=rows[0];const cells=firstRow.querySelectorAll('td');const salesNoCell=cells[2]?.innerText?.trim()||'';R.expectedSalesNo=cap.salesNo;R.actualSalesNo=salesNoCell;R.noChange=salesNoCell===cap.salesNo||salesNoCell.includes(cap.salesNo);R.ok=true;return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "VERIFY" + } + ] +} \ No newline at end of file diff --git a/edge-boundary-acc-sales.json b/edge-boundary-acc-sales.json new file mode 100644 index 0000000..2cee294 --- /dev/null +++ b/edge-boundary-acc-sales.json @@ -0,0 +1,122 @@ +{ + "id": "edge-boundary-acc-sales", + "name": "엣지 케이스: 경계값 입력 검증 (회계 > 매출관리)", + "version": "1.0.0", + "auth": { + "role": "admin" + }, + "menuNavigation": { + "level1": "회계관리", + "level2": "매출관리" + }, + "screenshotPolicy": { + "captureOnFail": true, + "captureOnPass": false + }, + "steps": [ + { + "id": 1, + "name": "[회계관리 > 매출관리] 페이지 로드 대기", + "action": "wait", + "timeout": 3000 + }, + { + "id": 2, + "name": "[회계관리 > 매출관리] 테이블 로드 대기", + "action": "wait_for_table", + "timeout": 8000 + }, + { + "id": 3, + "name": "[회계관리 > 매출관리] [EDGE] 등록 폼 열기", + "action": "evaluate", + "script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'OPEN_FORM'};const priorities=['매출 등록','매출등록','등록','추가','신규'];let btn=null;for(const kw of priorities){btn=Array.from(document.querySelectorAll('button')).find(b=>{const t=b.innerText?.trim()||'';return t.includes(kw)&&b.offsetParent!==null&&!b.disabled;});if(btn)break;}if(!btn){R.err='등록 버튼 없음';R.ok=true;return JSON.stringify(R);}R.btnText=btn.innerText?.trim();btn.click();await w(2500);R.url=location.pathname+location.search;R.ok=true;return JSON.stringify(R);})()", + "timeout": 15000, + "phase": "OPEN_FORM" + }, + { + "id": 4, + "name": "[회계관리 > 매출관리] [EDGE] 폼 렌더링 대기", + "action": "wait", + "timeout": 2000 + }, + { + "id": 5, + "name": "[회계관리 > 매출관리] [EDGE] 수량=0 입력 → 자동계산 반응 확인", + "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:'ZERO_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=>(i.placeholder||'').includes('수량')||(i.name||'').includes('quantity')||(i.name||'').includes('qty'));if(qtyInput){sv(qtyInput,'0');await w(500);R.qtySet=true;R.qtyValue=qtyInput.value;const supplyInputs=inputs.filter(i=>(i.placeholder||'').includes('공급')||(i.name||'').includes('supply')||(i.name||'').includes('amount'));R.supplyValue=supplyInputs.length>0?supplyInputs[0].value:'N/A';const hasError=document.querySelector('[class*=\"error\"],[class*=\"Error\"],[role=\"alert\"],.text-red-500,.text-destructive');R.errorShown=!!hasError;}else{R.qtyInputNotFound=true;const numInputs=inputs.filter(i=>i.type==='number'||(i.placeholder||'').match(/[0-9]/));if(numInputs.length>0){sv(numInputs[0],'0');await w(500);R.fallbackSet=true;}}R.ok=true;R.info=R.qtySet?'수량=0 입력 완료, 공급가액='+R.supplyValue:'수량 필드 미발견';return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "BOUNDARY" + }, + { + "id": 6, + "name": "[회계관리 > 매출관리] [EDGE] 수량=-1 입력 → 거부/에러 확인", + "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:'NEGATIVE_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=>(i.placeholder||'').includes('수량')||(i.name||'').includes('quantity')||(i.name||'').includes('qty'));if(qtyInput){sv(qtyInput,'-1');await w(500);R.qtySet=true;R.qtyValue=qtyInput.value;R.qtyAccepted=qtyInput.value==='-1';const hasError=document.querySelector('[class*=\"error\"],[class*=\"Error\"],[role=\"alert\"],.text-red-500,.text-destructive');R.errorShown=!!hasError;if(hasError){R.errorText=hasError.innerText?.trim().substring(0,80);}R.ariaInvalid=qtyInput.getAttribute('aria-invalid')==='true';}else{R.qtyInputNotFound=true;const numInputs=inputs.filter(i=>i.type==='number');if(numInputs.length>0){sv(numInputs[0],'-1');await w(500);R.fallbackSet=true;R.fallbackAccepted=numInputs[0].value==='-1';}}R.ok=true;R.info=R.errorShown?'✅ 음수 입력 시 에러 표시':'⚠️ 음수 입력 에러 미표시';return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "BOUNDARY" + }, + { + "id": 7, + "name": "[회계관리 > 매출관리] [EDGE] 단가=99999.99 소수점 입력 → 처리 확인", + "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:'DECIMAL_PRICE'};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 priceInput=inputs.find(i=>(i.placeholder||'').includes('단가')||(i.name||'').includes('price')||(i.name||'').includes('unitPrice'));if(priceInput){sv(priceInput,'99999.99');await w(500);R.priceSet=true;R.displayValue=priceInput.value;R.decimalKept=priceInput.value.includes('.');R.rounded=!priceInput.value.includes('.')&&priceInput.value!=='';const hasError=document.querySelector('[class*=\"error\"],[class*=\"Error\"],[role=\"alert\"],.text-red-500,.text-destructive');R.errorShown=!!hasError;}else{R.priceInputNotFound=true;const numInputs=inputs.filter(i=>i.type==='number'||(i.placeholder||'').match(/단가|금액|price/i));if(numInputs.length>1){sv(numInputs[1],'99999.99');await w(500);R.fallbackSet=true;R.fallbackValue=numInputs[1].value;}}R.ok=true;R.info=R.decimalKept?'소수점 유지됨: '+R.displayValue:(R.rounded?'소수점 반올림/제거됨: '+R.displayValue:'단가 필드 미발견');return JSON.stringify(R);})()", + "timeout": 10000, + "phase": "BOUNDARY" + }, + { + "id": 8, + "name": "[회계관리 > 매출관리] [EDGE] 품목명 255자 초과 입력 → 잘림/에러 확인", + "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:'MAX_LENGTH'};const longStr='E2E_TEST_LONG_'+'A'.repeat(260);const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const itemInput=inputs.find(i=>(i.placeholder||'').includes('품목')||(i.name||'').includes('item')||(i.name||'').includes('product'));if(itemInput){sv(itemInput,longStr);await w(500);R.inputSet=true;R.inputLength=itemInput.value.length;R.maxLengthAttr=itemInput.maxLength||'none';R.truncated=itemInput.value.length0){sv(inputs[0],longStr);await w(500);R.fallbackSet=true;R.fallbackLength=inputs[0].value.length;R.fallbackTruncated=inputs[0].value.length 매출관리] [EDGE] 특수문자/XSS 입력 → 방어 확인", + "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:'XSS_CHECK'};const xssPayload='';const inputs=Array.from(document.querySelectorAll('input[type=\"text\"],input:not([type]),textarea')).filter(i=>i.offsetParent!==null&&!i.readOnly&&!i.disabled);const itemInput=inputs.find(i=>(i.placeholder||'').includes('품목')||(i.name||'').includes('item')||(i.name||'').includes('product'));const targetInput=itemInput||inputs[0];if(targetInput){sv(targetInput,xssPayload);await w(500);R.inputSet=true;R.storedValue=targetInput.value;R.xssAccepted=targetInput.value.includes('