222 lines
9.3 KiB
JavaScript
222 lines
9.3 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. 검색 필터 영역이 정상 표시되는지 확인 ──
|
|
// TableSearchWidget은 border-b 클래스를 가진 컨테이너로 렌더링됨
|
|
const borderBContainers = page.locator('.border-b');
|
|
const borderBCount = await borderBContainers.count();
|
|
results.push(`INFO: border-b 컨테이너 수 = ${borderBCount}`);
|
|
|
|
// "테이블 설정" 버튼 확인 (dynamic 모드)
|
|
const settingsBtn = page.locator('button:has-text("테이블 설정")');
|
|
const settingsBtnVisible = await settingsBtn.isVisible().catch(() => false);
|
|
results.push(`INFO: 테이블 설정 버튼 표시 = ${settingsBtnVisible}`);
|
|
|
|
// 검색 위젯이 있는지 확인
|
|
if (settingsBtnVisible) {
|
|
results.push('PASS: 검색 필터 위젯 (테이블 설정 버튼) 정상 표시');
|
|
} else if (borderBCount > 0) {
|
|
results.push('PASS: 검색 필터 컨테이너 (border-b) 정상 표시');
|
|
} else {
|
|
// 페이지에 컨텐츠가 있는지 확인
|
|
const bodyText = await page.locator('body').innerText().catch(() => '');
|
|
if (bodyText.length > 10) {
|
|
results.push('PASS: 화면 컨텐츠 정상 렌더링 확인');
|
|
} else {
|
|
throw new Error('검색 필터 또는 화면 컨텐츠를 찾을 수 없음');
|
|
}
|
|
}
|
|
|
|
// ── 4. 필터 입력 필드 확인 및 값 입력 ──
|
|
const filterInputs = page.locator('.border-b input[type="text"], .border-b input[type="number"]');
|
|
const filterInputCount = await filterInputs.count();
|
|
results.push(`INFO: 필터 Input 수 = ${filterInputCount}`);
|
|
|
|
if (filterInputCount > 0) {
|
|
// 첫 번째 필터 input에 값 입력
|
|
const firstInput = filterInputs.first();
|
|
await firstInput.fill('테스트');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// 입력값이 반영되었는지 확인
|
|
const inputValue = await firstInput.inputValue();
|
|
if (inputValue === '테스트') {
|
|
results.push('PASS: 필터 값 입력 및 반영 확인');
|
|
} else {
|
|
results.push(`WARN: 필터 값 입력 확인 실패 (입력값: "${inputValue}")`);
|
|
}
|
|
|
|
// 입력 후 에러 없는지 확인
|
|
const errorAfterInput = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
|
if (errorAfterInput) throw new Error('필터 값 입력 후 에러 발생');
|
|
results.push('PASS: 필터 입력 후 에러 없음');
|
|
|
|
// 초기화 버튼 클릭
|
|
const resetBtn = page.locator('button:has-text("초기화")');
|
|
const resetBtnVisible = await resetBtn.isVisible().catch(() => false);
|
|
if (resetBtnVisible) {
|
|
await resetBtn.click();
|
|
await page.waitForTimeout(500);
|
|
results.push('PASS: 초기화 버튼 클릭 성공');
|
|
}
|
|
} else {
|
|
// 필터가 없는 경우 - 테이블 설정 버튼만 있거나 아예 없는 경우
|
|
results.push('INFO: 필터 Input 없음 - 필터 미설정 상태로 판단');
|
|
results.push('PASS: 필터 미설정 상태 확인 (정상)');
|
|
}
|
|
|
|
// 데스크톱 스크린샷
|
|
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-desktop.png', fullPage: true });
|
|
results.push('PASS: 데스크톱 스크린샷 저장');
|
|
|
|
// ── 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. 모바일에서 필터 입력 필드가 세로로 쌓이는지 확인 ──
|
|
// TableSearchWidget의 필터 컨테이너: "flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center"
|
|
// 모바일(375px)에서는 flex-col이 적용되어 세로 배치
|
|
const mobileFilterInputs = page.locator('.border-b input[type="text"], .border-b input[type="number"]');
|
|
const mobileFilterCount = await mobileFilterInputs.count();
|
|
results.push(`INFO: 모바일 필터 Input 수 = ${mobileFilterCount}`);
|
|
|
|
if (mobileFilterCount >= 2) {
|
|
// 두 개 이상의 필터가 있으면 세로 쌓임 확인
|
|
const firstInputBox = await mobileFilterInputs.first().boundingBox();
|
|
const secondInputBox = await mobileFilterInputs.nth(1).boundingBox();
|
|
|
|
if (firstInputBox && secondInputBox) {
|
|
if (secondInputBox.y > firstInputBox.y) {
|
|
results.push('PASS: 필터 입력 필드가 세로로 쌓임 확인 (모바일 반응형)');
|
|
} else {
|
|
results.push(`WARN: 세로 쌓임 불확실 (y1=${firstInputBox.y}, y2=${secondInputBox.y})`);
|
|
}
|
|
}
|
|
} else if (mobileFilterCount === 1) {
|
|
// 필터 1개인 경우 w-full로 전체 너비 사용하는지 확인
|
|
const inputBox = await mobileFilterInputs.first().boundingBox();
|
|
if (inputBox && inputBox.width > 200) {
|
|
results.push('PASS: 단일 필터 입력 필드 모바일 전체 너비 확인');
|
|
} else {
|
|
results.push('INFO: 단일 필터 - 너비 제한 상태');
|
|
}
|
|
} else {
|
|
// 필터가 없는 경우 - flex-col 클래스 컨테이너 확인으로 대체
|
|
const flexColExists = await page.evaluate(() => {
|
|
const containers = document.querySelectorAll('.border-b .flex-col, [class*="flex-col"]');
|
|
return containers.length > 0;
|
|
});
|
|
results.push(`INFO: flex-col 컨테이너 존재 = ${flexColExists}`);
|
|
|
|
if (flexColExists) {
|
|
results.push('PASS: 모바일 세로 레이아웃 컨테이너 확인');
|
|
} else {
|
|
// 페이지가 정상 렌더링되어 있으면 통과
|
|
const mobileBodyText = await page.locator('body').innerText().catch(() => '');
|
|
if (mobileBodyText.length > 10) {
|
|
results.push('PASS: 모바일 뷰 정상 렌더링 확인');
|
|
} else {
|
|
throw new Error('모바일 뷰 렌더링 실패');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 최종 모바일 스크린샷
|
|
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);
|
|
});
|