Files
sam-hotfix/e2e/runner/gen-pagination-sort.js

222 lines
11 KiB
JavaScript
Raw Normal View History

#!/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.ok=true;`,
`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.ok=true;`,
`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();