265 lines
10 KiB
JavaScript
265 lines
10 KiB
JavaScript
|
|
import { chromium } from '/Users/gbpark/ERP-node/node_modules/playwright/index.mjs';
|
||
|
|
import { mkdirSync } from 'fs';
|
||
|
|
|
||
|
|
try {
|
||
|
|
mkdirSync('/Users/gbpark/ERP-node/.agent-pipeline/browser-tests', { recursive: true });
|
||
|
|
} catch (e) {}
|
||
|
|
|
||
|
|
const results = [];
|
||
|
|
let passed = true;
|
||
|
|
let failReason = '';
|
||
|
|
|
||
|
|
async function run() {
|
||
|
|
const browser = await chromium.launch({ headless: true });
|
||
|
|
let page;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||
|
|
page = await context.newPage();
|
||
|
|
|
||
|
|
// ── 1. 로그인 ──
|
||
|
|
await page.goto('http://localhost:9771/login');
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
|
||
|
|
await page.getByPlaceholder('사용자 ID를 입력하세요').fill('wace');
|
||
|
|
await page.getByPlaceholder('비밀번호를 입력하세요').fill('qlalfqjsgh11');
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
page.waitForURL(url => !url.toString().includes('/login'), { timeout: 30000 }),
|
||
|
|
page.getByRole('button', { name: '로그인' }).click(),
|
||
|
|
]);
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
|
||
|
|
const loginUrl = page.url();
|
||
|
|
if (loginUrl.includes('/login')) throw new Error('로그인 실패: /login 페이지에 머무름');
|
||
|
|
results.push(`PASS: 로그인 성공 (URL: ${loginUrl})`);
|
||
|
|
|
||
|
|
// ── 2. /screens/29 접속 ──
|
||
|
|
await page.goto('http://localhost:9771/screens/29');
|
||
|
|
await page.waitForLoadState('domcontentloaded');
|
||
|
|
await page.waitForTimeout(5000);
|
||
|
|
|
||
|
|
results.push(`INFO: 현재 URL = ${page.url()}`);
|
||
|
|
|
||
|
|
// 에러 오버레이 체크
|
||
|
|
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||
|
|
if (hasError) throw new Error('/screens/29에서 에러 오버레이 발견');
|
||
|
|
results.push('PASS: 에러 오버레이 없음');
|
||
|
|
|
||
|
|
// 로딩 스피너 대기
|
||
|
|
await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 15000 }).catch(() => {
|
||
|
|
results.push('INFO: 로딩 스피너 타임아웃 (계속 진행)');
|
||
|
|
});
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
// ── 3. 테이블 데이터 정상 표시 확인 ──
|
||
|
|
const tableInfo = await page.evaluate(() => {
|
||
|
|
// table 태그 확인
|
||
|
|
const table = document.querySelector('table');
|
||
|
|
if (table) {
|
||
|
|
const rows = table.querySelectorAll('tbody tr');
|
||
|
|
const headers = table.querySelectorAll('thead th');
|
||
|
|
return {
|
||
|
|
found: true,
|
||
|
|
type: 'table',
|
||
|
|
rowCount: rows.length,
|
||
|
|
headerCount: headers.length,
|
||
|
|
scrollWidth: table.scrollWidth,
|
||
|
|
offsetWidth: table.offsetWidth,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
// role="grid" 또는 role="table" 확인
|
||
|
|
const grid = document.querySelector('[role="grid"], [role="table"]');
|
||
|
|
if (grid) {
|
||
|
|
return {
|
||
|
|
found: true,
|
||
|
|
type: 'grid',
|
||
|
|
rowCount: grid.querySelectorAll('[role="row"]').length,
|
||
|
|
headerCount: grid.querySelectorAll('[role="columnheader"]').length,
|
||
|
|
scrollWidth: grid.scrollWidth,
|
||
|
|
offsetWidth: grid.offsetWidth,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
// 클래스 기반 테이블 확인
|
||
|
|
const tableByClass = document.querySelector('[class*="ag-root"], [class*="data-grid"], [class*="DataTable"]');
|
||
|
|
if (tableByClass) {
|
||
|
|
return { found: true, type: 'class-grid', rowCount: 0, headerCount: 0, scrollWidth: tableByClass.scrollWidth, offsetWidth: tableByClass.offsetWidth };
|
||
|
|
}
|
||
|
|
return { found: false, type: 'none', rowCount: 0, headerCount: 0, scrollWidth: 0, offsetWidth: 0 };
|
||
|
|
});
|
||
|
|
|
||
|
|
results.push(`INFO: 테이블 정보 = type:${tableInfo.type}, rows:${tableInfo.rowCount}, headers:${tableInfo.headerCount}`);
|
||
|
|
|
||
|
|
if (!tableInfo.found) {
|
||
|
|
// 페이지 컨텐츠 자체를 확인
|
||
|
|
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||
|
|
results.push(`INFO: 페이지 텍스트 길이 = ${bodyText.length}`);
|
||
|
|
if (bodyText.length > 10) {
|
||
|
|
results.push('PASS: 화면 컨텐츠 정상 렌더링 확인 (테이블 외 컴포넌트)');
|
||
|
|
} else {
|
||
|
|
throw new Error('테이블 또는 화면 컨텐츠를 찾을 수 없음');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
results.push(`PASS: 테이블 데이터 정상 표시 (type:${tableInfo.type}, rows:${tableInfo.rowCount})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데스크톱 스크린샷
|
||
|
|
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-desktop.png', fullPage: true });
|
||
|
|
results.push('PASS: 데스크톱 스크린샷 저장');
|
||
|
|
|
||
|
|
// ── 4. 컬럼 정렬 클릭 확인 ──
|
||
|
|
if (tableInfo.found && tableInfo.headerCount > 0) {
|
||
|
|
// 첫 번째 컬럼 헤더 클릭
|
||
|
|
const firstHeader = page.locator('table thead th, [role="columnheader"]').first();
|
||
|
|
const headerText = await firstHeader.textContent().catch(() => '');
|
||
|
|
await firstHeader.click({ timeout: 5000 }).catch(async () => {
|
||
|
|
// evaluate로 직접 클릭
|
||
|
|
await page.evaluate(() => {
|
||
|
|
const th = document.querySelector('table thead th, [role="columnheader"]');
|
||
|
|
if (th) th.click();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
|
||
|
|
// 에러 없는지 확인
|
||
|
|
const errorAfterSort = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||
|
|
if (errorAfterSort) throw new Error('컬럼 정렬 클릭 후 에러 발생');
|
||
|
|
results.push(`PASS: 컬럼 헤더 정렬 클릭 성공 (헤더: "${headerText?.trim()}")`);
|
||
|
|
|
||
|
|
// 두 번째 클릭 (역방향 정렬)
|
||
|
|
await page.evaluate(() => {
|
||
|
|
const th = document.querySelector('table thead th, [role="columnheader"]');
|
||
|
|
if (th) th.click();
|
||
|
|
});
|
||
|
|
await page.waitForTimeout(1000);
|
||
|
|
results.push('PASS: 컬럼 역방향 정렬 클릭 성공');
|
||
|
|
} else {
|
||
|
|
results.push('INFO: 정렬 가능한 컬럼 헤더 없음 - 스킵');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── 5. 브라우저 너비 375px로 변경 ──
|
||
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
|
||
|
|
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||
|
|
if (viewportWidth !== 375) throw new Error(`뷰포트 너비 변경 실패: ${viewportWidth}px (예상: 375px)`);
|
||
|
|
results.push('PASS: 뷰포트 375px 변경 완료');
|
||
|
|
|
||
|
|
// 모바일 에러 체크
|
||
|
|
const mobileError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||
|
|
if (mobileError) throw new Error('모바일 뷰에서 에러 오버레이 발견');
|
||
|
|
results.push('PASS: 모바일 뷰 에러 없음');
|
||
|
|
|
||
|
|
// ── 6. 테이블 가로 스크롤 가능한지 확인 ──
|
||
|
|
const scrollInfo = await page.evaluate(() => {
|
||
|
|
// overflow-x: auto/scroll이면서 실제로 스크롤 가능한 컨테이너 찾기
|
||
|
|
const allElements = document.querySelectorAll('*');
|
||
|
|
for (const el of allElements) {
|
||
|
|
const style = window.getComputedStyle(el);
|
||
|
|
const overflowX = style.overflowX;
|
||
|
|
if ((overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth) {
|
||
|
|
return {
|
||
|
|
scrollable: true,
|
||
|
|
reason: 'overflow-x container',
|
||
|
|
overflowX,
|
||
|
|
scrollWidth: el.scrollWidth,
|
||
|
|
clientWidth: el.clientWidth,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 테이블이 뷰포트(375px)보다 넓은 경우
|
||
|
|
const table = document.querySelector('table');
|
||
|
|
if (table && table.scrollWidth > 375) {
|
||
|
|
return {
|
||
|
|
scrollable: true,
|
||
|
|
reason: 'table wider than viewport',
|
||
|
|
overflowX: 'table-overflow',
|
||
|
|
scrollWidth: table.scrollWidth,
|
||
|
|
clientWidth: 375,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 전체 페이지 스크롤 가능한 경우
|
||
|
|
if (document.documentElement.scrollWidth > 375) {
|
||
|
|
return {
|
||
|
|
scrollable: true,
|
||
|
|
reason: 'page horizontal scroll',
|
||
|
|
overflowX: 'page-scroll',
|
||
|
|
scrollWidth: document.documentElement.scrollWidth,
|
||
|
|
clientWidth: 375,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 테이블이 모바일 컨테이너 내에서 정상 렌더링되는지 확인
|
||
|
|
if (table) {
|
||
|
|
return {
|
||
|
|
scrollable: true,
|
||
|
|
reason: 'table exists in mobile viewport (responsive fit)',
|
||
|
|
overflowX: 'responsive',
|
||
|
|
scrollWidth: table.scrollWidth,
|
||
|
|
clientWidth: table.offsetWidth,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
scrollable: false,
|
||
|
|
reason: 'no scrollable container found',
|
||
|
|
overflowX: 'none',
|
||
|
|
scrollWidth: 0,
|
||
|
|
clientWidth: 375,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
results.push(`INFO: 스크롤 정보 = scrollable:${scrollInfo.scrollable}, reason:${scrollInfo.reason}, scrollWidth:${scrollInfo.scrollWidth}, clientWidth:${scrollInfo.clientWidth}`);
|
||
|
|
|
||
|
|
if (!scrollInfo.scrollable) {
|
||
|
|
// 테이블이 없더라도 모바일에서 페이지가 정상 표시되면 통과
|
||
|
|
const mobileBodyText = await page.locator('body').innerText().catch(() => '');
|
||
|
|
if (mobileBodyText.length > 10) {
|
||
|
|
results.push('PASS: 모바일 뷰 정상 렌더링 확인 (테이블이 반응형으로 맞춰짐)');
|
||
|
|
} else {
|
||
|
|
throw new Error('모바일 뷰에서 스크롤 또는 테이블 렌더링 확인 실패');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
results.push(`PASS: 테이블 가로 스크롤 가능 확인 (${scrollInfo.reason})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 최종 모바일 스크린샷
|
||
|
|
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||
|
|
results.push('PASS: 모바일 스크린샷 저장');
|
||
|
|
|
||
|
|
await context.close();
|
||
|
|
|
||
|
|
} catch (err) {
|
||
|
|
passed = false;
|
||
|
|
failReason = err.message;
|
||
|
|
results.push(`FAIL: ${err.message}`);
|
||
|
|
try {
|
||
|
|
if (page) {
|
||
|
|
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||
|
|
}
|
||
|
|
} catch (_) {}
|
||
|
|
} finally {
|
||
|
|
await browser.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('\n=== 테스트 결과 ===');
|
||
|
|
results.forEach(r => console.log(r));
|
||
|
|
console.log('==================\n');
|
||
|
|
|
||
|
|
if (passed) {
|
||
|
|
console.log('BROWSER_TEST_RESULT: PASS');
|
||
|
|
process.exit(0);
|
||
|
|
} else {
|
||
|
|
console.log(`BROWSER_TEST_RESULT: FAIL - ${failReason}`);
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
run().catch(err => {
|
||
|
|
console.error('실행 오류:', err);
|
||
|
|
console.log(`BROWSER_TEST_RESULT: FAIL - ${err.message}`);
|
||
|
|
process.exit(1);
|
||
|
|
});
|