/** * 거래처관리 화면 CRUD 브라우저 테스트 * 실행: node scripts/browser-test-customer-crud.js * 브라우저 표시: HEADLESS=0 node scripts/browser-test-customer-crud.js */ const { chromium } = require("playwright"); const BASE_URL = "http://localhost:9771"; const SCREENSHOT_DIR = "test-screenshots"; async function runTest() { const results = { success: [], failed: [], screenshots: [] }; const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" }); const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); const page = await context.newPage(); try { // 스크린샷 디렉토리 const fs = require("fs"); if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); const screenshot = async (name) => { const path = `${SCREENSHOT_DIR}/${name}.png`; await page.screenshot({ path, fullPage: true }); results.screenshots.push(path); console.log(` [스크린샷] ${path}`); }; console.log("\n=== 1단계: 로그인 ===\n"); await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 }); await page.fill('#userId', 'topseal_admin'); await page.fill('#password', 'qlalfqjsgh11'); await page.click('button[type="submit"]'); await page.waitForTimeout(3000); await screenshot("01_after_login"); results.success.push("로그인 완료"); console.log("\n=== 2단계: 거래처관리 화면 이동 ===\n"); await page.goto(`${BASE_URL}/screens/227`, { waitUntil: "domcontentloaded", timeout: 20000 }); // 테이블 또는 메인 콘텐츠 로딩 대기 (API 호출 후 React 렌더링) try { await page.waitForSelector('table, tbody, [role="row"], .rt-tbody', { timeout: 25000 }); results.success.push("테이블 로드 감지"); } catch (e) { console.log(" [경고] 테이블 대기 타임아웃, 계속 진행"); } await page.waitForTimeout(3000); await screenshot("02_screen_227"); results.success.push("화면 227 로드"); console.log("\n=== 3단계: 거래처 선택 (READ 테스트) ===\n"); // 좌측 테이블 행 선택 - 다양한 레이아웃 대응 const rowSelectors = [ 'table tbody tr.cursor-pointer', 'tbody tr.hover\\:bg-accent', 'table tbody tr:has(td)', 'tbody tr', ]; let rows = []; for (const sel of rowSelectors) { rows = await page.$$(sel); if (rows.length > 0) break; } if (rows.length > 0) { await rows[0].click(); results.success.push("거래처 행 클릭"); } else { results.failed.push("거래처 테이블 행을 찾을 수 없음"); // 디버그: 페이지 구조 저장 const bodyHtml = await page.evaluate(() => { const tables = document.querySelectorAll('table, tbody, [role="grid"], [role="table"]'); return `Tables found: ${tables.length}\n` + document.body.innerHTML.slice(0, 8000); }); require("fs").writeFileSync(`${SCREENSHOT_DIR}/debug_body.html`, bodyHtml); console.log(" [디버그] body HTML 일부 저장: debug_body.html"); } await page.waitForTimeout(3000); await screenshot("03_after_customer_select"); // SelectedItemsDetailInput 영역 확인 const detailArea = await page.$('[data-component="selected-items-detail-input"], [class*="selected-items"], .selected-items-detail'); if (detailArea) { results.success.push("SelectedItemsDetailInput 컴포넌트 렌더링 확인"); } else { // 품목/입력 관련 영역이 있는지 const hasInputArea = await page.$('input[placeholder*="품번"], input[placeholder*="품목"], [class*="detail"]'); results.success.push(hasInputArea ? "입력 영역 확인됨" : "SelectedItemsDetailInput 영역 미확인"); } console.log("\n=== 4단계: 품목 추가 (CREATE 테스트) ===\n"); const addBtnLoc = page.locator('button').filter({ hasText: /추가|품목/ }).first(); const addBtnExists = await addBtnLoc.count() > 0; if (addBtnExists) { await addBtnLoc.click(); await page.waitForTimeout(1500); await screenshot("04_after_add_click"); // 모달/팝업에서 품목 선택 const modalItem = await page.$('[role="dialog"] tr, [role="listbox"] [role="option"], .modal tbody tr'); if (modalItem) { await modalItem.click(); await page.waitForTimeout(1000); } // 필수 필드 입력 const itemCodeInput = await page.$('input[name*="품번"], input[placeholder*="품번"], input[id*="item"]'); if (itemCodeInput) { await itemCodeInput.fill("TEST_BROWSER"); } await screenshot("04_before_save"); const saveBtnLoc = page.locator('button').filter({ hasText: /저장/ }).first(); if (await saveBtnLoc.count() > 0) { await saveBtnLoc.click(); await page.waitForTimeout(3000); await screenshot("05_after_save"); results.success.push("저장 버튼 클릭"); const toast = await page.$('[data-sonner-toast], .toast, [role="alert"]'); if (toast) { const toastText = await toast.textContent(); results.success.push(`토스트 메시지: ${toastText?.slice(0, 50)}`); } } else { results.failed.push("저장 버튼을 찾을 수 없음"); } } else { results.failed.push("품목 추가/추가 버튼을 찾을 수 없음"); await screenshot("04_no_add_button"); } console.log("\n=== 5단계: 최종 결과 ===\n"); await screenshot("06_final_state"); // 콘솔 에러 수집 const consoleErrors = []; page.on("console", (msg) => { const type = msg.type(); if (type === "error") { consoleErrors.push(msg.text()); } }); } catch (err) { results.failed.push(`예외: ${err.message}`); try { await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true }); results.screenshots.push(`${SCREENSHOT_DIR}/error.png`); } catch (_) {} } finally { await browser.close(); } // 결과 출력 console.log("\n========== 테스트 결과 ==========\n"); console.log("성공:", results.success); console.log("실패:", results.failed); console.log("스크린샷:", results.screenshots); return results; } runTest().then((r) => { process.exit(r.failed.length > 0 ? 1 : 0); }).catch((e) => { console.error(e); process.exit(1); });