- gen-*.js: 시나리오 자동 생성기 12종 (CRUD, edge, a11y, perf 등) - search-*.js: 검색/버튼 감사 수집기 3종 - revert-hard-actions.js: 하드 액션 복원 유틸 - _gen_writer.py: 생성기 보조 스크립트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
253 lines
13 KiB
JavaScript
253 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 검색 기능 실제 동작 테스트 시나리오 생성기
|
|
*
|
|
* 기존 search-options는 검색 UI 존재 여부만 감사.
|
|
* 이 테스트는 실제로 검색어를 입력하고 결과가 필터링되는지 검증.
|
|
*
|
|
* 흐름: 기존 데이터 캡처 → 검색어 입력 → 필터링 확인 → 초기화 → 원복 확인
|
|
*
|
|
* Usage: node e2e/runner/gen-search-function.js
|
|
*
|
|
* Output:
|
|
* e2e/scenarios/search-function-acc.json (어음/입금/거래처)
|
|
* e2e/scenarios/search-function-sales.json (거래처/수주/견적)
|
|
* e2e/scenarios/search-function-hr-board.json (사원/자유게시판)
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const SCENARIOS_DIR = path.resolve(__dirname, '..', 'scenarios');
|
|
|
|
const H = `const w=ms=>new Promise(r=>setTimeout(r,ms));`;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 검색 기능 테스트 스크립트 (모든 페이지 공용)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
// STEP 1: 검색 전 상태 캡처 + 검색어 추출
|
|
const CAPTURE_AND_SEARCH = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'SEARCH'};`,
|
|
// 1. 현재 테이블 상태 캡처
|
|
`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);}`,
|
|
// 2. 첫 행에서 검색 키워드 추출 (2번째 셀 = 보통 이름/제목)
|
|
`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);}`,
|
|
// 3. 검색 입력란 찾기
|
|
`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||'';`,
|
|
// 4. 검색어 입력
|
|
`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}));`,
|
|
// 5. Enter 키로 검색 트리거
|
|
`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);`,
|
|
// 6. 필터링 결과 확인
|
|
`const rows1=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`,
|
|
`R.afterSearchRowCount=rows1.length;`,
|
|
// 7. 결과 행이 모두 키워드를 포함하는지 확인
|
|
`const matchingRows=rows1.filter(r=>r.innerText?.includes(keyword));`,
|
|
`R.matchingRowCount=matchingRows.length;`,
|
|
`R.allMatch=rows1.length>0&&matchingRows.length===rows1.length;`,
|
|
// 8. 필터링 분석
|
|
`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);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// STEP 2: 검색 초기화 + 원복 확인
|
|
const CLEAR_AND_VERIFY = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'CLEAR'};`,
|
|
// 1. 검색 입력란 찾기
|
|
`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);}`,
|
|
// 2. 현재 행 수 기록
|
|
`const rowsBefore=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null).length;`,
|
|
`R.beforeClearRows=rowsBefore;`,
|
|
// 3. 검색어 지우기
|
|
`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);`,
|
|
// 4. 초기화 버튼이 있으면 클릭
|
|
`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;}`,
|
|
// 5. 원복 확인
|
|
`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);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// STEP 3: 드롭다운 필터 테스트 (combobox 선택 → 필터 확인 → 원복)
|
|
const DROPDOWN_FILTER_TEST = [
|
|
`(async()=>{`, H,
|
|
`const R={phase:'DROPDOWN_FILTER'};`,
|
|
// 1. 현재 행 수
|
|
`const rows0=Array.from(document.querySelectorAll('table tbody tr')).filter(r=>r.offsetParent!==null);`,
|
|
`R.initialRows=rows0.length;`,
|
|
// 2. 드롭다운(combobox) 찾기
|
|
`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);}`,
|
|
// 3. 첫 번째 드롭다운 클릭 → 두 번째 옵션 선택 (첫 번째는 보통 "전체")
|
|
`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));`,
|
|
// 4. 두 번째 옵션 선택 (첫 번째가 "전체"일 수 있으므로)
|
|
`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);`,
|
|
// 5. 필터 결과 확인
|
|
`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='드롭다운 선택 후 행 수 변화 없음 (해당 필터의 모든 데이터일 수 있음)';`,
|
|
// 6. 원복: 첫 번째 옵션("전체")으로 복원
|
|
`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);}`,
|
|
// 7. 원복 확인
|
|
`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);`,
|
|
`})()`,
|
|
].join('');
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PAGE GROUPS
|
|
// ════════════════════════════════════════════════════════════════
|
|
const GROUPS = [
|
|
{
|
|
id: 'search-function-acc',
|
|
name: '검색 기능 동작 검증: 회계',
|
|
pages: [
|
|
{ level1: '회계관리', level2: '어음관리' },
|
|
{ level1: '회계관리', level2: '입금관리' },
|
|
{ level1: '회계관리', level2: '거래처관리' },
|
|
],
|
|
},
|
|
{
|
|
id: 'search-function-sales',
|
|
name: '검색 기능 동작 검증: 판매',
|
|
pages: [
|
|
{ level1: '판매관리', level2: '거래처관리' },
|
|
{ level1: '판매관리', level2: '수주관리' },
|
|
{ level1: '판매관리', level2: '견적관리' },
|
|
],
|
|
},
|
|
{
|
|
id: 'search-function-hr-board',
|
|
name: '검색 기능 동작 검증: 인사/게시판',
|
|
pages: [
|
|
{ level1: '인사관리', level2: '사원관리' },
|
|
{ level1: '게시판', level2: '자유게시판' },
|
|
],
|
|
},
|
|
];
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
function generateScenario(group) {
|
|
const steps = [];
|
|
let id = 1;
|
|
|
|
for (let pi = 0; pi < group.pages.length; pi++) {
|
|
const page = group.pages[pi];
|
|
const pfx = `[${page.level1} > ${page.level2}]`;
|
|
|
|
if (pi > 0) {
|
|
steps.push({ id: id++, name: `${pfx} 메뉴 이동`, action: 'menu_navigate', level1: page.level1, level2: page.level2, timeout: 10000 });
|
|
}
|
|
|
|
steps.push({ id: id++, name: `${pfx} 페이지 로드 대기`, action: 'wait', timeout: 3000 });
|
|
steps.push({ id: id++, name: `${pfx} 테이블 로드 대기`, action: 'wait_for_table', timeout: 5000 });
|
|
|
|
// 텍스트 검색 테스트
|
|
steps.push({ id: id++, name: `${pfx} 텍스트 검색 테스트`, action: 'evaluate', script: CAPTURE_AND_SEARCH, timeout: 15000, phase: 'SEARCH' });
|
|
|
|
// 검색 초기화 테스트
|
|
steps.push({ id: id++, name: `${pfx} 검색 초기화 확인`, action: 'evaluate', script: CLEAR_AND_VERIFY, timeout: 10000, phase: 'CLEAR' });
|
|
|
|
// 안정화 대기
|
|
steps.push({ id: id++, name: `${pfx} 초기화 후 안정화`, action: 'wait', timeout: 2000 });
|
|
|
|
// 드롭다운 필터 테스트
|
|
steps.push({ id: id++, name: `${pfx} 드롭다운 필터 테스트`, action: 'evaluate', script: DROPDOWN_FILTER_TEST, timeout: 20000, phase: 'FILTER' });
|
|
}
|
|
|
|
return {
|
|
id: group.id,
|
|
name: group.name,
|
|
version: '1.0.0',
|
|
auth: { role: 'admin' },
|
|
menuNavigation: group.pages[0],
|
|
screenshotPolicy: { captureOnFail: true, captureOnPass: false },
|
|
steps,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
if (!fs.existsSync(SCENARIOS_DIR)) fs.mkdirSync(SCENARIOS_DIR, { recursive: true });
|
|
for (const group of GROUPS) {
|
|
const scenario = generateScenario(group);
|
|
const fp = path.join(SCENARIOS_DIR, `${scenario.id}.json`);
|
|
fs.writeFileSync(fp, JSON.stringify(scenario, null, 2), 'utf-8');
|
|
console.log(` ${scenario.id}.json (${scenario.steps.length} steps)`);
|
|
}
|
|
console.log(`\n Generated ${GROUPS.length} scenarios\n Run: node e2e/runner/run-all.js --filter search-function-`);
|
|
}
|
|
|
|
main();
|