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); });