#!/usr/bin/env node /** * 테이블 페이지네이션 & 정렬 검증 시나리오 생성기 * * 실제 고객이 가장 많이 사용하는 기능: 목록 페이지 이동, 컬럼 정렬. * 버그 발견 포인트: 중복 데이터, 정렬 미동작, 페이지간 데이터 불일치, 빈 페이지. * * Usage: node e2e/runner/gen-pagination-sort.js * * Output: * e2e/scenarios/pagination-sort-acc.json * e2e/scenarios/pagination-sort-sales.json * e2e/scenarios/pagination-sort-hr.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));`; // ── 페이지네이션 검증 스크립트 ────────────────────────────────── // 1) 현재 페이지 행 수 확인 // 2) 다음 페이지 버튼 클릭 // 3) 데이터 변경 확인 (1페이지와 2페이지 첫 행이 달라야 함) // 4) 이전 페이지 복귀 확인 const PAGINATION_TEST = [ `(async()=>{`, H, `const R={phase:'PAGINATION'};`, // Get current page data `const rows1=Array.from(document.querySelectorAll('table tbody tr'));`, `R.page1RowCount=rows1.length;`, `R.page1FirstRow=rows1[0]?.innerText?.substring(0,60)||'';`, `R.page1LastRow=rows1[rows1.length-1]?.innerText?.substring(0,60)||'';`, // Find pagination controls `const paginationBtns=Array.from(document.querySelectorAll('button,a,[role="button"]')).filter(b=>{`, ` const t=b.innerText?.trim()||'';const al=b.getAttribute('aria-label')||'';`, ` return(/^[2-9]$|^\\d{2,}$/.test(t)||/next|다음|chevron.?right|›|»|>/.test(t+al+b.className))&&b.offsetParent!==null;`, `});`, `R.paginationBtnCount=paginationBtns.length;`, // Also check for shadcn pagination: nav[aria-label] with buttons `const navPagination=document.querySelector('nav[aria-label*="pagination"],nav[aria-label*="page"]');`, `if(navPagination){`, ` const navBtns=Array.from(navPagination.querySelectorAll('button,a')).filter(b=>b.offsetParent!==null);`, ` R.navPaginationBtns=navBtns.length;`, `}`, // Find "next page" button `let nextBtn=paginationBtns.find(b=>{const t=(b.innerText?.trim()||'')+(b.getAttribute('aria-label')||'');return/next|다음|›|»|chevron.?right/i.test(t+b.className);});`, `if(!nextBtn)nextBtn=paginationBtns.find(b=>b.innerText?.trim()==='2');`, `if(!nextBtn&&navPagination){nextBtn=Array.from(navPagination.querySelectorAll('button,a')).find(b=>/next|다음|›|»/i.test((b.innerText||'')+(b.getAttribute('aria-label')||'')+b.className)&&b.offsetParent!==null);}`, `R.hasNextBtn=!!nextBtn;`, `if(!nextBtn){R.warn='페이지네이션 버튼 없음 (데이터 부족 또는 미구현)';R.ok=true;return JSON.stringify(R);}`, // Click next page `nextBtn.click();await w(2000);`, `const rows2=Array.from(document.querySelectorAll('table tbody tr'));`, `R.page2RowCount=rows2.length;`, `R.page2FirstRow=rows2[0]?.innerText?.substring(0,60)||'';`, // Verify different data `R.dataChanged=R.page1FirstRow!==R.page2FirstRow;`, `R.hasRows=R.page2RowCount>0;`, `if(!R.dataChanged&&R.hasRows)R.warn='⚠️ 페이지 변경 후 동일 데이터 표시 (페이지네이션 미동작 의심)';`, `if(!R.hasRows)R.warn='⚠️ 2페이지에 데이터 없음';`, // Go back to page 1 `const prevBtn=Array.from(document.querySelectorAll('button,a,[role="button"]')).find(b=>{const t=(b.innerText?.trim()||'')+(b.getAttribute('aria-label')||'');return(/prev|이전|‹|«|chevron.?left/i.test(t+b.className)||b.innerText?.trim()==='1')&&b.offsetParent!==null;});`, `if(!prevBtn&&navPagination){const pb=Array.from(navPagination.querySelectorAll('button,a')).find(b=>/prev|이전|‹|«/i.test((b.innerText||'')+(b.getAttribute('aria-label')||'')+b.className)&&b.offsetParent!==null);if(pb)pb.click();}`, `else if(prevBtn){prevBtn.click();}`, `await w(1500);`, `const rows3=Array.from(document.querySelectorAll('table tbody tr'));`, `R.backToPage1=rows3[0]?.innerText?.substring(0,60)===R.page1FirstRow;`, `if(!R.backToPage1)R.warn=(R.warn||'')+' ⚠️ 1페이지 복귀 후 데이터 불일치';`, `R.paginationWorked=R.dataChanged&&R.hasRows;`, `R.ok=R.paginationWorked!==false;`, `return JSON.stringify(R);`, `})()`, ].join(''); // ── 컬럼 정렬 검증 스크립트 ────────────────────────────────── // 1) 테이블 헤더 클릭 (정렬) // 2) 정렬 후 데이터 순서 변경 확인 // 3) 다시 클릭 (역순 정렬) 확인 const SORT_TEST = [ `(async()=>{`, H, `const R={phase:'SORT'};`, // Get sortable headers `const headers=Array.from(document.querySelectorAll('table thead th,table thead td,[role="columnheader"]'));`, `R.headerCount=headers.length;`, `R.headerTexts=headers.map(h=>h.innerText?.trim()).filter(t=>t).slice(0,10);`, // Find a clickable header (exclude checkbox column) `const sortableHeaders=headers.filter(h=>{`, ` const t=h.innerText?.trim()||'';`, ` return t.length>0&&!h.querySelector('input[type="checkbox"]')&&h.offsetParent!==null;`, `});`, `R.sortableCount=sortableHeaders.length;`, `if(sortableHeaders.length===0){R.warn='정렬 가능한 헤더 없음';R.ok=true;return JSON.stringify(R);}`, // Get pre-sort data (first column values) `const getFirstColValues=()=>Array.from(document.querySelectorAll('table tbody tr')).slice(0,5).map(r=>{const cells=r.querySelectorAll('td');return(cells[1]||cells[0])?.innerText?.trim().substring(0,30)||'';});`, `R.beforeSort=getFirstColValues();`, // Click first text header to sort `const targetHeader=sortableHeaders.find(h=>h.innerText?.trim().length>1)||sortableHeaders[0];`, `R.sortColumn=targetHeader.innerText?.trim();`, `targetHeader.click();await w(1500);`, `R.afterSort1=getFirstColValues();`, `R.sortChanged1=JSON.stringify(R.beforeSort)!==JSON.stringify(R.afterSort1);`, // Click again for reverse sort `targetHeader.click();await w(1500);`, `R.afterSort2=getFirstColValues();`, `R.sortChanged2=JSON.stringify(R.afterSort1)!==JSON.stringify(R.afterSort2);`, // Analysis `if(!R.sortChanged1&&!R.sortChanged2)R.warn='⚠️ 컬럼 클릭 후 정렬 변화 없음 (정렬 미구현 의심)';`, `else if(R.sortChanged1&&!R.sortChanged2)R.warn='⚠️ 역순 정렬 미동작 (한방향만 정렬)';`, `R.sortWorked=R.sortChanged1||R.sortChanged2;`, `R.ok=R.sortWorked!==false;`, `return JSON.stringify(R);`, `})()`, ].join(''); // ── 행 수 일관성 검증 (데이터 무결성) ──────────────────────── const ROW_COUNT_CHECK = [ `(async()=>{`, H, `const R={phase:'ROW_COUNT'};`, `const rows=document.querySelectorAll('table tbody tr');`, `R.rowCount=rows.length;`, // Check for pagination info text (e.g., "1-20 of 150") `const pageInfo=document.body.innerText.match(new RegExp('(\\\\d+)\\\\s*[-~]\\\\s*(\\\\d+)\\\\s*(of|중|개|건|/|총)\\\\s*(\\\\d+)','i'));`, `if(pageInfo){R.pageInfoText=pageInfo[0];R.totalItems=parseInt(pageInfo[4]);}`, // Check for "total" badge or count `const totalBadge=Array.from(document.querySelectorAll('[class*="badge"],[class*="count"],[class*="total"]')).find(e=>/\\d+/.test(e.innerText));`, `if(totalBadge)R.totalBadgeText=totalBadge.innerText?.trim();`, // Check if rows have empty cells (possible rendering bug) `const emptyRows=Array.from(rows).filter(r=>r.innerText?.trim().length===0);`, `R.emptyRowCount=emptyRows.length;`, `if(emptyRows.length>0)R.warn='⚠️ 빈 행 '+emptyRows.length+'개 발견 (렌더링 버그 의심)';`, // Check for duplicate rows `const rowTexts=Array.from(rows).map(r=>r.innerText?.trim().substring(0,80));`, `const duplicates=rowTexts.filter((t,i)=>rowTexts.indexOf(t)!==i);`, `R.duplicateCount=duplicates.length;`, `if(duplicates.length>0)R.warn=(R.warn||'')+' ⚠️ 중복 행 '+duplicates.length+'개 발견';`, `R.ok=true;`, `return JSON.stringify(R);`, `})()`, ].join(''); // ════════════════════════════════════════════════════════════════ // PAGE GROUPS // ════════════════════════════════════════════════════════════════ const GROUPS = [ { id: 'pagination-sort-acc', name: '페이지네이션 & 정렬 검증: 회계', pages: [ { level1: '회계관리', level2: '어음관리' }, { level1: '회계관리', level2: '입금관리' }, { level1: '회계관리', level2: '거래처관리' }, ], }, { id: 'pagination-sort-sales', name: '페이지네이션 & 정렬 검증: 판매', pages: [ { level1: '판매관리', level2: '거래처관리' }, { level1: '판매관리', level2: '수주관리' }, { level1: '판매관리', level2: '견적관리' }, ], }, { id: 'pagination-sort-hr', 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: ROW_COUNT_CHECK, timeout: 10000, phase: 'VERIFY' }); // 정렬 테스트 steps.push({ id: id++, name: `${pfx} 컬럼 정렬 검증`, action: 'evaluate', script: SORT_TEST, timeout: 15000, phase: 'SORT' }); // 페이지네이션 테스트 steps.push({ id: id++, name: `${pfx} 페이지네이션 검증`, action: 'evaluate', script: PAGINATION_TEST, timeout: 20000, phase: 'PAGINATION' }); } 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 pagination-sort`); } main();