feat: 검색 기능 실제 동작 테스트 시나리오 추가 (텍스트 검색 + 드롭다운 필터 + 초기화 검증, 3/3 PASS)

This commit is contained in:
김보곤
2026-02-12 10:38:18 +09:00
parent 39c74a8ee8
commit edb3debabe
3 changed files with 430 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
{
"id": "search-function-hr-board",
"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": 5000
},
{
"id": 3,
"name": "[인사관리 > 사원관리] 텍스트 검색 테스트",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SEARCH'};const rows0=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.initialRowCount=rows0.length;if(rows0.length===0){R.warn='테이블에 데이터 없음 - 검색 테스트 불가';R.ok=true;return JSON.stringify(R);}const cells=rows0[0].querySelectorAll('td');let keyword='';for(let i=1;i<cells.length&&i<5;i++){ const t=cells[i]?.innerText?.trim(); if(t&&t.length>=2&&t.length<=20&&!/^\\d+$/.test(t)&&!/^\\d{4}[-/]/.test(t)){keyword=t;break;}}if(!keyword&&cells[0]){keyword=cells[0]?.innerText?.trim().substring(0,10);}R.keyword=keyword;if(!keyword||keyword.length<2){R.warn='검색 가능한 키워드 추출 실패';R.ok=true;return JSON.stringify(R);}const searchInput=document.querySelector('input[placeholder*=\"검색\"]')||document.querySelector('input[type=\"search\"]')||document.querySelector('input[role=\"searchbox\"]');R.hasSearchInput=!!searchInput;if(!searchInput){R.warn='검색 입력란 없음';R.ok=true;return JSON.stringify(R);}R.placeholder=searchInput.placeholder||'';searchInput.focus();await w(200);const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(searchInput,keyword);else searchInput.value=keyword;searchInput.dispatchEvent(new Event('input',{bubbles:true}));searchInput.dispatchEvent(new Event('change',{bubbles:true}));searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));searchInput.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));await w(2000);const rows1=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.afterSearchRowCount=rows1.length;const matchingRows=rows1.filter(r=>r.innerText?.includes(keyword));R.matchingRowCount=matchingRows.length;R.allMatch=rows1.length>0&&matchingRows.length===rows1.length;R.filtered=R.afterSearchRowCount<=R.initialRowCount;if(R.afterSearchRowCount===R.initialRowCount&&R.initialRowCount>1){ R.filterWorked=R.allMatch; if(!R.allMatch)R.info='검색 후 행 수 동일 - 모든 행이 키워드 포함 또는 검색 미동작';}else if(R.afterSearchRowCount<R.initialRowCount){ R.filterWorked=true; R.info='검색 필터링 동작 확인 ('+R.initialRowCount+'→'+R.afterSearchRowCount+'행)';}else if(R.afterSearchRowCount===0){ R.filterWorked=false; R.warn='⚠️ 검색 후 결과 0건 - 기존 데이터의 키워드인데 결과 없음';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000,
"phase": "SEARCH"
},
{
"id": 4,
"name": "[인사관리 > 사원관리] 검색 초기화 확인",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CLEAR'};const searchInput=document.querySelector('input[placeholder*=\"검색\"]')||document.querySelector('input[type=\"search\"]')||document.querySelector('input[role=\"searchbox\"]');if(!searchInput){R.warn='검색 입력란 없음';R.ok=true;return JSON.stringify(R);}const rowsBefore=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null).length;R.beforeClearRows=rowsBefore;const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(searchInput,'');else searchInput.value='';searchInput.dispatchEvent(new Event('input',{bubbles:true}));searchInput.dispatchEvent(new Event('change',{bubbles:true}));searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));await w(2000);const resetBtn=Array.from(document.querySelectorAll('button')).find(b=>/초기화|리셋|전체|Reset|Clear/i.test(b.innerText?.trim())&&b.offsetParent!==null);if(resetBtn){resetBtn.click();await w(1500);R.resetBtnClicked=true;}const rowsAfter=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null).length;R.afterClearRows=rowsAfter;R.restored=rowsAfter>=rowsBefore;R.searchInputValue=searchInput.value;R.inputCleared=searchInput.value===''||searchInput.value.length===0;if(!R.restored&&rowsAfter<rowsBefore)R.warn='⚠️ 검색 초기화 후 행 수가 줄었음 ('+rowsBefore+'→'+rowsAfter+')';R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "CLEAR"
},
{
"id": 5,
"name": "[인사관리 > 사원관리] 초기화 후 안정화",
"action": "wait",
"timeout": 2000
},
{
"id": 6,
"name": "[인사관리 > 사원관리] 드롭다운 필터 테스트",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DROPDOWN_FILTER'};const rows0=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.initialRows=rows0.length;const combos=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>{ const inTable=b.closest('table'); const inModal=b.closest('[role=\"dialog\"],[aria-modal=\"true\"]'); return b.offsetParent!==null&&!inTable&&!inModal;});R.comboCount=combos.length;if(combos.length===0){R.warn='필터용 드롭다운 없음';R.ok=true;return JSON.stringify(R);}const targetCombo=combos[0];R.comboLabel=targetCombo.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText?.trim()||'';R.comboCurrentValue=targetCombo.innerText?.trim().substring(0,30);targetCombo.click();await w(600);const listbox=document.querySelector('[role=\"listbox\"]');if(!listbox){R.warn='드롭다운 열림 실패';R.ok=true;return JSON.stringify(R);}const options=Array.from(listbox.querySelectorAll('[role=\"option\"]'));R.optionCount=options.length;R.optionTexts=options.slice(0,5).map(o=>o.innerText?.trim().substring(0,20));const targetOpt=options.length>1?options[1]:options[0];if(!targetOpt){document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));R.warn='선택 가능한 옵션 없음';R.ok=true;return JSON.stringify(R);}R.selectedOption=targetOpt.innerText?.trim().substring(0,20);targetOpt.click();await w(2000);const rows1=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.afterFilterRows=rows1.length;R.filterChanged=R.afterFilterRows!==R.initialRows;if(!R.filterChanged&&R.initialRows>1)R.info='드롭다운 선택 후 행 수 변화 없음 (해당 필터의 모든 데이터일 수 있음)';targetCombo.click();await w(600);const listbox2=document.querySelector('[role=\"listbox\"]');if(listbox2){ const allOpt=Array.from(listbox2.querySelectorAll('[role=\"option\"]')).find(o=>/전체|All|선택/i.test(o.innerText?.trim())); if(allOpt){allOpt.click();await w(1500);R.restored=true;} else{const firstOpt=listbox2.querySelector('[role=\"option\"]');if(firstOpt){firstOpt.click();await w(1500);R.restored=true;}}}if(!listbox2){document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(500);}const rows2=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.afterRestoreRows=rows2.length;R.ok=true;return JSON.stringify(R);})()",
"timeout": 20000,
"phase": "FILTER"
},
{
"id": 7,
"name": "[게시판 > 자유게시판] 메뉴 이동",
"action": "menu_navigate",
"level1": "게시판",
"level2": "자유게시판",
"timeout": 10000
},
{
"id": 8,
"name": "[게시판 > 자유게시판] 페이지 로드 대기",
"action": "wait",
"timeout": 3000
},
{
"id": 9,
"name": "[게시판 > 자유게시판] 테이블 로드 대기",
"action": "wait_for_table",
"timeout": 5000
},
{
"id": 10,
"name": "[게시판 > 자유게시판] 텍스트 검색 테스트",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'SEARCH'};const rows0=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.initialRowCount=rows0.length;if(rows0.length===0){R.warn='테이블에 데이터 없음 - 검색 테스트 불가';R.ok=true;return JSON.stringify(R);}const cells=rows0[0].querySelectorAll('td');let keyword='';for(let i=1;i<cells.length&&i<5;i++){ const t=cells[i]?.innerText?.trim(); if(t&&t.length>=2&&t.length<=20&&!/^\\d+$/.test(t)&&!/^\\d{4}[-/]/.test(t)){keyword=t;break;}}if(!keyword&&cells[0]){keyword=cells[0]?.innerText?.trim().substring(0,10);}R.keyword=keyword;if(!keyword||keyword.length<2){R.warn='검색 가능한 키워드 추출 실패';R.ok=true;return JSON.stringify(R);}const searchInput=document.querySelector('input[placeholder*=\"검색\"]')||document.querySelector('input[type=\"search\"]')||document.querySelector('input[role=\"searchbox\"]');R.hasSearchInput=!!searchInput;if(!searchInput){R.warn='검색 입력란 없음';R.ok=true;return JSON.stringify(R);}R.placeholder=searchInput.placeholder||'';searchInput.focus();await w(200);const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(searchInput,keyword);else searchInput.value=keyword;searchInput.dispatchEvent(new Event('input',{bubbles:true}));searchInput.dispatchEvent(new Event('change',{bubbles:true}));searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));searchInput.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));await w(2000);const rows1=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.afterSearchRowCount=rows1.length;const matchingRows=rows1.filter(r=>r.innerText?.includes(keyword));R.matchingRowCount=matchingRows.length;R.allMatch=rows1.length>0&&matchingRows.length===rows1.length;R.filtered=R.afterSearchRowCount<=R.initialRowCount;if(R.afterSearchRowCount===R.initialRowCount&&R.initialRowCount>1){ R.filterWorked=R.allMatch; if(!R.allMatch)R.info='검색 후 행 수 동일 - 모든 행이 키워드 포함 또는 검색 미동작';}else if(R.afterSearchRowCount<R.initialRowCount){ R.filterWorked=true; R.info='검색 필터링 동작 확인 ('+R.initialRowCount+'→'+R.afterSearchRowCount+'행)';}else if(R.afterSearchRowCount===0){ R.filterWorked=false; R.warn='⚠️ 검색 후 결과 0건 - 기존 데이터의 키워드인데 결과 없음';}R.ok=true;return JSON.stringify(R);})()",
"timeout": 15000,
"phase": "SEARCH"
},
{
"id": 11,
"name": "[게시판 > 자유게시판] 검색 초기화 확인",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'CLEAR'};const searchInput=document.querySelector('input[placeholder*=\"검색\"]')||document.querySelector('input[type=\"search\"]')||document.querySelector('input[role=\"searchbox\"]');if(!searchInput){R.warn='검색 입력란 없음';R.ok=true;return JSON.stringify(R);}const rowsBefore=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null).length;R.beforeClearRows=rowsBefore;const nativeSetter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')?.set;if(nativeSetter)nativeSetter.call(searchInput,'');else searchInput.value='';searchInput.dispatchEvent(new Event('input',{bubbles:true}));searchInput.dispatchEvent(new Event('change',{bubbles:true}));searchInput.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,bubbles:true}));await w(2000);const resetBtn=Array.from(document.querySelectorAll('button')).find(b=>/초기화|리셋|전체|Reset|Clear/i.test(b.innerText?.trim())&&b.offsetParent!==null);if(resetBtn){resetBtn.click();await w(1500);R.resetBtnClicked=true;}const rowsAfter=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null).length;R.afterClearRows=rowsAfter;R.restored=rowsAfter>=rowsBefore;R.searchInputValue=searchInput.value;R.inputCleared=searchInput.value===''||searchInput.value.length===0;if(!R.restored&&rowsAfter<rowsBefore)R.warn='⚠️ 검색 초기화 후 행 수가 줄었음 ('+rowsBefore+'→'+rowsAfter+')';R.ok=true;return JSON.stringify(R);})()",
"timeout": 10000,
"phase": "CLEAR"
},
{
"id": 12,
"name": "[게시판 > 자유게시판] 초기화 후 안정화",
"action": "wait",
"timeout": 2000
},
{
"id": 13,
"name": "[게시판 > 자유게시판] 드롭다운 필터 테스트",
"action": "evaluate",
"script": "(async()=>{const w=ms=>new Promise(r=>setTimeout(r,ms));const R={phase:'DROPDOWN_FILTER'};const rows0=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.initialRows=rows0.length;const combos=Array.from(document.querySelectorAll('button[role=\"combobox\"]')).filter(b=>{ const inTable=b.closest('table'); const inModal=b.closest('[role=\"dialog\"],[aria-modal=\"true\"]'); return b.offsetParent!==null&&!inTable&&!inModal;});R.comboCount=combos.length;if(combos.length===0){R.warn='필터용 드롭다운 없음';R.ok=true;return JSON.stringify(R);}const targetCombo=combos[0];R.comboLabel=targetCombo.closest('[class*=field],[class*=Field],[class*=form-item]')?.querySelector('label')?.innerText?.trim()||'';R.comboCurrentValue=targetCombo.innerText?.trim().substring(0,30);targetCombo.click();await w(600);const listbox=document.querySelector('[role=\"listbox\"]');if(!listbox){R.warn='드롭다운 열림 실패';R.ok=true;return JSON.stringify(R);}const options=Array.from(listbox.querySelectorAll('[role=\"option\"]'));R.optionCount=options.length;R.optionTexts=options.slice(0,5).map(o=>o.innerText?.trim().substring(0,20));const targetOpt=options.length>1?options[1]:options[0];if(!targetOpt){document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));R.warn='선택 가능한 옵션 없음';R.ok=true;return JSON.stringify(R);}R.selectedOption=targetOpt.innerText?.trim().substring(0,20);targetOpt.click();await w(2000);const rows1=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.afterFilterRows=rows1.length;R.filterChanged=R.afterFilterRows!==R.initialRows;if(!R.filterChanged&&R.initialRows>1)R.info='드롭다운 선택 후 행 수 변화 없음 (해당 필터의 모든 데이터일 수 있음)';targetCombo.click();await w(600);const listbox2=document.querySelector('[role=\"listbox\"]');if(listbox2){ const allOpt=Array.from(listbox2.querySelectorAll('[role=\"option\"]')).find(o=>/전체|All|선택/i.test(o.innerText?.trim())); if(allOpt){allOpt.click();await w(1500);R.restored=true;} else{const firstOpt=listbox2.querySelector('[role=\"option\"]');if(firstOpt){firstOpt.click();await w(1500);R.restored=true;}}}if(!listbox2){document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}));await w(500);}const rows2=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);R.afterRestoreRows=rows2.length;R.ok=true;return JSON.stringify(R);})()",
"timeout": 20000,
"phase": "FILTER"
}
]
}