/** * 결재함 플로우 E2E 테스트 스크립트 * 실행: npx tsx scripts/approval-flow-test.ts */ import { chromium } from "playwright"; const BASE_URL = "http://localhost:9771"; const LOGIN_ID = "wace"; const LOGIN_PW = "1234"; const FALLBACK_PW = "qlalfqjsgh11"; // 마스터 패스워드 (1234 실패 시) async function main() { const results: string[] = []; const consoleErrors: string[] = []; const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, // 데스크톱 뷰 (사이드바 표시) }); const page = await context.newPage(); // 콘솔 에러 수집 page.on("console", (msg) => { const type = msg.type(); if (type === "error") { const text = msg.text(); consoleErrors.push(text); } }); try { // 1. http://localhost:9771 이동 results.push("=== 1. http://localhost:9771 이동 ==="); await page.goto(BASE_URL, { waitUntil: "networkidle", timeout: 15000 }); results.push("OK: 페이지 로드 완료"); // 2. 로그인 여부 확인 results.push("\n=== 2. 로그인 상태 확인 ==="); const isLoginPage = await page.locator('#userId, input[name="userId"]').count() > 0; if (isLoginPage) { results.push("로그인 페이지 감지됨. 로그인 시도..."); await page.fill('#userId', LOGIN_ID); await page.fill('#password', LOGIN_PW); await page.click('button[type="submit"]'); await page.waitForTimeout(4000); // 여전히 로그인 페이지면 마스터 패스워드로 재시도 const stillLoginPage = await page.locator('#userId').count() > 0; if (stillLoginPage) { results.push("1234 로그인 실패. 마스터 패스워드로 재시도..."); await page.fill('#userId', LOGIN_ID); await page.fill('#password', FALLBACK_PW); await page.click('button[type="submit"]'); await page.waitForTimeout(4000); } results.push("로그인 폼 제출 완료"); } else { results.push("이미 로그인된 상태로 판단 (로그인 폼 없음)"); } // 3. 사용자 프로필 아바타 클릭 (사이드바 하단) results.push("\n=== 3. 사용자 프로필 아바타 클릭 ==="); await page.waitForTimeout(2000); // 사이드바 하단 사용자 프로필 버튼 (border-t border-slate-200 내부의 button) const sidebarAvatarBtn = page.locator('aside div.border-t.border-slate-200 button').first(); let avatarClicked = false; if ((await sidebarAvatarBtn.count()) > 0) { try { // force: true - Next.js dev overlay가 클릭을 가로채는 경우 우회 await sidebarAvatarBtn.click({ timeout: 5000, force: true }); avatarClicked = true; results.push("OK: 사이드바 하단 아바타 클릭 완료"); await page.waitForTimeout(500); // 드롭다운 열림 대기 } catch (e) { results.push(`WARN: 사이드바 아바타 클릭 실패 - ${(e as Error).message}`); } } if (!avatarClicked) { // 모바일 헤더 아바타 또는 fallback const headerAvatar = page.locator('header button:has(div.rounded-full)').first(); if ((await headerAvatar.count()) > 0) { await headerAvatar.click({ force: true }); avatarClicked = true; results.push("OK: 헤더 아바타 클릭 (모바일 뷰?)"); } } if (!avatarClicked) { results.push("WARN: 아바타 클릭 실패. 직접 /admin/approvalBox로 이동하여 페이지 검증"); await page.goto(`${BASE_URL}/admin/approvalBox`, { waitUntil: "networkidle", timeout: 10000 }); } await page.waitForTimeout(1000); // 4. "결재함" 메뉴 확인 (드롭다운이 열린 경우) results.push("\n=== 4. 결재함 메뉴 확인 ==="); const approvalMenuItem = page.locator('[role="menuitem"]:has-text("결재함"), [data-radix-collection-item]:has-text("결재함")').first(); const hasApprovalMenu = (await approvalMenuItem.count()) > 0; if (hasApprovalMenu) { results.push("OK: 결재함 메뉴가 보입니다."); } else { results.push("FAIL: 결재함 메뉴를 찾을 수 없습니다."); } // 5. 결재함 메뉴 클릭 results.push("\n=== 5. 결재함 메뉴 클릭 ==="); if (hasApprovalMenu) { await approvalMenuItem.click({ force: true }); await page.waitForTimeout(3000); results.push("OK: 결재함 메뉴 클릭 완료"); } else if (!avatarClicked) { results.push("(직접 이동으로 스킵 - 이미 approvalBox 페이지)"); } else { results.push("WARN: 드롭다운에서 결재함 메뉴 미발견. 직접 이동..."); await page.goto(`${BASE_URL}/admin/approvalBox`, { waitUntil: "networkidle", timeout: 10000 }); await page.waitForTimeout(2000); } // 6. /admin/approvalBox 페이지 렌더링 확인 results.push("\n=== 6. /admin/approvalBox 페이지 확인 ==="); const currentUrl = page.url(); const isApprovalBoxPage = currentUrl.includes("/admin/approvalBox"); results.push(`현재 URL: ${currentUrl}`); results.push(isApprovalBoxPage ? "OK: approvalBox 페이지에 있습니다." : "FAIL: approvalBox 페이지가 아닙니다."); // 제목 "결재함" 확인 const titleEl = page.locator('h1:has-text("결재함")'); const hasTitle = (await titleEl.count()) > 0; results.push(hasTitle ? "OK: 제목 '결재함' 확인됨" : "FAIL: 제목 '결재함' 없음"); // 탭 확인: 수신함, 상신함 const receivedTab = page.locator('button[role="tab"], [role="tab"]').filter({ hasText: "수신함" }); const sentTab = page.locator('button[role="tab"], [role="tab"]').filter({ hasText: "상신함" }); const hasReceivedTab = (await receivedTab.count()) > 0; const hasSentTab = (await sentTab.count()) > 0; results.push(hasReceivedTab ? "OK: '수신함' 탭 확인됨" : "FAIL: '수신함' 탭 없음"); results.push(hasSentTab ? "OK: '상신함' 탭 확인됨" : "FAIL: '상신함' 탭 없음"); // 7. 콘솔 에러 확인 results.push("\n=== 7. 콘솔 에러 확인 ==="); if (consoleErrors.length === 0) { results.push("OK: 콘솔 에러 없음"); } else { results.push(`WARN: 콘솔 에러 ${consoleErrors.length}건 발견:`); consoleErrors.slice(0, 10).forEach((err, i) => { results.push(` [${i + 1}] ${err.substring(0, 200)}${err.length > 200 ? "..." : ""}`); }); if (consoleErrors.length > 10) { results.push(` ... 외 ${consoleErrors.length - 10}건`); } } // 스크린샷 저장 (프로젝트 내) await page.screenshot({ path: "approval-box-result.png" }).catch(() => {}); } catch (err: any) { results.push(`\nERROR: ${err.message}`); } finally { await browser.close(); } // 결과 출력 console.log("\n" + "=".repeat(60)); console.log("결재함 플로우 테스트 결과"); console.log("=".repeat(60)); results.forEach((r) => console.log(r)); console.log("\n" + "=".repeat(60)); } main();