diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index f5469d35..00000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -export default defineConfig({ - testDir: "./tests/e2e", - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: 0, - workers: 1, - reporter: "html", - timeout: 60_000, - expect: { timeout: 10_000 }, - - use: { - baseURL: "http://localhost:9771", - trace: "on-first-retry", - screenshot: "only-on-failure", - video: "on-first-retry", - actionTimeout: 15_000, - }, - - projects: [ - { - name: "chromium", - use: { - ...devices["Desktop Chrome"], - viewport: { width: 1024, height: 800 }, - }, - }, - ], -}); diff --git a/frontend/tests/e2e/pop-cart-outbound.spec.ts b/frontend/tests/e2e/pop-cart-outbound.spec.ts deleted file mode 100644 index 2fb0072a..00000000 --- a/frontend/tests/e2e/pop-cart-outbound.spec.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { test, expect, Page } from "@playwright/test"; - -// ============================================ -// 판매출고 POP 화면 (4578) 브라우저 테스트 -// ============================================ -// -// 테스트 흐름: -// 1. 로그인 -> POP 화면 접속 -// 2. 화면 렌더링 확인 -// 3. 거래처 검색(모달) -> 거래처 선택 -> 카드 로드 -// 4. 카드 컴포넌트 표시 확인 -// 5. 수량 입력 + 담기 동작 -// 6. 장바구니 취소 동작 -// ============================================ - -const TEST_CONFIG = { - screenId: 4578, - loginUrl: "/login", - screenUrl: "/pop/screens/4578", - credentials: { - userId: "topseal_admin", - password: "qlalfqjsgh11", - }, - // 테스트 거래처 (DB에 미리 삽입됨) - testCustomer: { - code: "T-CUST-001", - name: "(T)테스트거래처", - }, -}; - -// 로그인 헬퍼 -async function login(page: Page) { - await page.goto(TEST_CONFIG.loginUrl); - await page.waitForLoadState("networkidle"); - - // 로그인 폼 입력 - await page.locator("#userId").fill(TEST_CONFIG.credentials.userId); - await page.locator("#password").fill(TEST_CONFIG.credentials.password); - await page.locator('button[type="submit"]').click(); - - // 로그인 성공 대기 (리다이렉트) - await page.waitForURL((url) => !url.pathname.includes("/login"), { - timeout: 15_000, - }); -} - -// POP 화면 이동 헬퍼 -async function navigateToPopScreen(page: Page) { - await page.goto(TEST_CONFIG.screenUrl); - await page.waitForLoadState("networkidle"); - - // 로딩 스피너가 사라질 때까지 대기 - await page - .locator(".animate-spin") - .waitFor({ state: "hidden", timeout: 15_000 }) - .catch(() => { - /* 이미 로드 완료 */ - }); -} - -// 거래처 선택 헬퍼: 모달 검색으로 테스트 거래처 선택 -async function selectTestCustomer(page: Page) { - // comp_4 (pop-search modal) - 거래처 검색 클릭 - // 모달형 검색은 role="button"인 div, placeholder="거래처 검색" 텍스트 포함 - const searchTrigger = page.locator( - '[role="button"]:has-text("거래처 검색"), [role="button"]:has-text("선택...")' - ).first(); - - const isTriggerVisible = await searchTrigger - .isVisible({ timeout: 5_000 }) - .catch(() => false); - - if (!isTriggerVisible) { - // 대안: placeholder를 포함하는 input/button 찾기 - const altTrigger = page - .locator('text="거래처 검색"') - .first(); - if (await altTrigger.isVisible().catch(() => false)) { - await altTrigger.click(); - } else { - console.log("WARN: 거래처 검색 트리거를 찾을 수 없음"); - return false; - } - } else { - await searchTrigger.click(); - } - - // 모달 다이얼로그가 열릴 때까지 대기 - const dialog = page.locator('[role="dialog"]'); - await dialog.waitFor({ state: "visible", timeout: 10_000 }); - - // 필터 탭("가나다" 등)이 활성화되어 있으면 비활성화 - // "(T)테스트거래처"는 "("로 시작하므로 한글 초성 필터에서 제외됨 - const filterTabButton = dialog.locator('button:has-text("가나다")'); - if (await filterTabButton.isVisible({ timeout: 2_000 }).catch(() => false)) { - // 활성 상태(variant=default) 확인 후 클릭해서 토글 off - await filterTabButton.click(); - await page.waitForTimeout(300); - } - - // 모달에서 테스트 거래처 검색 - const modalSearchInput = dialog.locator('input[type="text"]').first(); - if (await modalSearchInput.isVisible({ timeout: 3_000 }).catch(() => false)) { - await modalSearchInput.fill("테스트거래처"); - await page.waitForTimeout(800); // 디바운스 대기 - } - - // "(T)테스트거래처" 항목 클릭 - 아이콘 뷰에서는 truncated text 내 span - const customerItem = dialog - .locator(`text=${TEST_CONFIG.testCustomer.name}`) - .first(); - let found = await customerItem - .isVisible({ timeout: 5_000 }) - .catch(() => false); - - // 아이콘 뷰에서 truncate 때문에 정확한 텍스트 매칭 실패 시 role=button으로 시도 - if (!found) { - // 부분 텍스트 매칭 시도 - const partialMatch = dialog.locator('text=테스트거래처').first(); - found = await partialMatch.isVisible({ timeout: 2_000 }).catch(() => false); - if (found) { - await partialMatch.click(); - await dialog.waitFor({ state: "hidden", timeout: 5_000 }).catch(() => {}); - await page.waitForTimeout(1_500); - return true; - } - - // role=button 중 텍스트 포함된 것 시도 - const iconBtn = dialog.locator('[role="button"]').filter({ hasText: /테스트/ }).first(); - found = await iconBtn.isVisible({ timeout: 2_000 }).catch(() => false); - if (found) { - await iconBtn.click(); - await dialog.waitFor({ state: "hidden", timeout: 5_000 }).catch(() => {}); - await page.waitForTimeout(1_500); - return true; - } - } - - if (found) { - await customerItem.click(); - // 모달 닫힘 + 데이터 로딩 대기 - await dialog - .waitFor({ state: "hidden", timeout: 5_000 }) - .catch(() => {}); - await page.waitForTimeout(1_500); // API 응답 + 렌더링 대기 - return true; - } - - console.log("WARN: 테스트 거래처를 모달에서 찾을 수 없음"); - return false; -} - -// ============================================ -// 테스트 시작 -// ============================================ - -test.describe("판매출고 POP 화면 (pop-cart-outbound)", () => { - test.beforeEach(async ({ page }) => { - await login(page); - }); - - // ------------------------------------------- - // 시나리오 1: 화면 접속 + 렌더링 확인 - // ------------------------------------------- - test("화면 접속 시 POP 화면이 정상 렌더링된다", async ({ page }) => { - await navigateToPopScreen(page); - - // 에러 화면이 아닌지 확인 - const errorText = page.getByText("화면을 찾을 수 없습니다"); - await expect(errorText).not.toBeVisible(); - - // 빈 화면이 아닌지 확인 - const emptyText = page.getByText("화면이 비어있습니다"); - await expect(emptyText).not.toBeVisible(); - - // 페이지가 정상 로드됨 - await expect(page).toHaveURL(/\/pop\/screens\/4578/); - - // 스크린샷 - await page.screenshot({ - path: "tests/e2e/screenshots/01-screen-loaded.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 2: 카트 아웃바운드 컴포넌트 확인 - // ------------------------------------------- - test("pop-cart-outbound 컴포넌트가 화면에 표시된다", async ({ page }) => { - await navigateToPopScreen(page); - - // requireFilter 설정으로 "거래처를 선택하면" 메시지 표시됨 - const hasEmptyState = await page - .locator("text=거래처를 선택하면") - .isVisible({ timeout: 5_000 }) - .catch(() => false); - - const hasHeader = await page - .locator("text=출고 대상 품목") - .first() - .isVisible() - .catch(() => false); - - const hasCards = await page - .locator("div.rounded-xl.border") - .first() - .isVisible() - .catch(() => false); - - // 셋 중 하나는 있어야 함 (컴포넌트가 렌더링됨) - expect(hasHeader || hasEmptyState || hasCards).toBeTruthy(); - - await page.screenshot({ - path: "tests/e2e/screenshots/02-cart-outbound-visible.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 3: 검색 컴포넌트 확인 - // ------------------------------------------- - test("검색/스캔 컴포넌트가 화면에 표시된다", async ({ page }) => { - await navigateToPopScreen(page); - - // input 또는 검색 관련 요소가 있는지 확인 - const searchInputs = page.locator( - 'input[type="text"], input[placeholder*="검색"], input[placeholder*="스캔"], [role="button"]:has-text("거래처"), [role="button"]:has-text("선택")' - ); - const searchCount = await searchInputs.count(); - - // 최소 1개 이상의 검색/입력 요소가 있어야 함 - expect(searchCount).toBeGreaterThanOrEqual(1); - - await page.screenshot({ - path: "tests/e2e/screenshots/03-search-components.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 4: 거래처 선택 -> 카드 로드 - // ------------------------------------------- - test("거래처 선택 시 출고 대상 품목 카드가 표시된다", async ({ page }) => { - await navigateToPopScreen(page); - - // 거래처 선택 전: 빈 상태 메시지 확인 - const emptyMsg = page.locator("text=거래처를 선택하면"); - const hasEmptyMsg = await emptyMsg - .isVisible({ timeout: 3_000 }) - .catch(() => false); - if (hasEmptyMsg) { - console.log("INFO: requireFilter 동작 확인 - 거래처 미선택 시 빈 상태 메시지 표시"); - } - - await page.screenshot({ - path: "tests/e2e/screenshots/04a-before-customer-select.png", - fullPage: true, - }); - - // 거래처 선택 - const selected = await selectTestCustomer(page); - if (!selected) { - test.skip(true, "테스트 거래처를 선택할 수 없음"); - return; - } - - // 카드가 로딩되는지 확인 - // 테스트 데이터: "(T)테스트 알루미늄 프로파일", "(T)테스트 스틸바 SB-200" - const card = page - .locator("text=테스트 알루미늄, text=테스트 스틸바, text=T-ITEM") - .first(); - const hasCards = await card - .isVisible({ timeout: 8_000 }) - .catch(() => false); - - if (hasCards) { - console.log("SUCCESS: 거래처 선택 후 품목 카드 로딩됨"); - } else { - // 카드가 안 보일 수 있음 - 스크린샷으로 디버그 - console.log("WARN: 거래처 선택 후 카드가 즉시 보이지 않음 - 스크린샷 확인 필요"); - } - - await page.screenshot({ - path: "tests/e2e/screenshots/04b-after-customer-select.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 5: 카드에 담기 버튼 존재 확인 - // ------------------------------------------- - test("품목 카드에 담기 버튼이 있다", async ({ page }) => { - await navigateToPopScreen(page); - - // 먼저 거래처 선택으로 카드 로드 - const selected = await selectTestCustomer(page); - if (!selected) { - test.skip(true, "테스트 거래처를 선택할 수 없음 - 담기 버튼 테스트 스킵"); - return; - } - - // 담기 버튼 확인 - const addButton = page.getByRole("button", { name: /담기/ }).first(); - const hasData = await addButton - .isVisible({ timeout: 5_000 }) - .catch(() => false); - - if (hasData) { - await expect(addButton).toBeVisible(); - await expect(addButton).toBeEnabled(); - console.log("SUCCESS: 담기 버튼 확인됨"); - } else { - console.log("INFO: 품목 데이터 없음 - 빈 상태 확인"); - test.skip(true, "테스트 데이터 없음 - 담기 버튼 테스트 스킵"); - } - - await page.screenshot({ - path: "tests/e2e/screenshots/05-add-button.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 6: 수량 입력 모달 열기 - // ------------------------------------------- - test("수량 버튼 클릭 시 숫자 입력 모달이 열린다", async ({ page }) => { - await navigateToPopScreen(page); - - // 거래처 선택 - const selected = await selectTestCustomer(page); - if (!selected) { - test.skip(true, "테스트 거래처를 선택할 수 없음"); - return; - } - - // 수량 버튼 찾기 (숫자 + 단위 표시된 버튼) - const qtyButton = page - .locator("button") - .filter({ hasText: /\d+.*?(EA|kg|개|톤|m|L)/i }) - .first(); - const hasQtyButton = await qtyButton - .isVisible({ timeout: 5_000 }) - .catch(() => false); - - if (!hasQtyButton) { - test.skip(true, "수량 버튼을 찾을 수 없음"); - return; - } - - // 수량 버튼 클릭 - await qtyButton.click(); - - // 모달이 열렸는지 확인 (숫자 키패드 또는 확인 버튼) - const confirmButton = page.getByRole("button", { name: /확인/ }); - await expect(confirmButton).toBeVisible({ timeout: 5_000 }); - - // 키패드 숫자 버튼이 있는지 - const numberButton = page - .getByRole("button", { name: /^[0-9]$/ }) - .first(); - await expect(numberButton).toBeVisible(); - - await page.screenshot({ - path: "tests/e2e/screenshots/06-number-modal.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 7: 수량 입력 후 담기 전체 흐름 - // ------------------------------------------- - test("수량 입력 -> 담기 -> 장바구니 저장 흐름이 동작한다", async ({ - page, - }) => { - await navigateToPopScreen(page); - - // 거래처 선택 - const selected = await selectTestCustomer(page); - if (!selected) { - test.skip(true, "테스트 거래처를 선택할 수 없음"); - return; - } - - // 1. 담기 버튼이 있는지 확인 - const addButton = page.getByRole("button", { name: /담기/ }).first(); - const hasData = await addButton - .isVisible({ timeout: 5_000 }) - .catch(() => false); - - if (!hasData) { - test.skip(true, "테스트 데이터 없음 - 담기 흐름 테스트 스킵"); - return; - } - - // 2. 수량 버튼 클릭 -> 모달 열기 - const qtyButton = page - .locator("button") - .filter({ hasText: /\d+.*?(EA|kg|개|톤|m|L)/i }) - .first(); - if (await qtyButton.isVisible()) { - await qtyButton.click(); - - // 3. 숫자 입력 (1, 0 -> 10) - const numBtn1 = page.getByRole("button", { name: "1" }); - const numBtn0 = page.getByRole("button", { name: "0" }); - if (await numBtn1.isVisible({ timeout: 3_000 }).catch(() => false)) { - await numBtn1.click(); - await numBtn0.click(); - } - - // 4. 확인 클릭 - const confirmBtn = page.getByRole("button", { name: /확인/ }); - if (await confirmBtn.isVisible()) { - await confirmBtn.click(); - // 모달 닫힘 대기 - await confirmBtn - .waitFor({ state: "hidden", timeout: 5_000 }) - .catch(() => {}); - } - } - - // 5. 담기 버튼 클릭 - await addButton.click(); - - // 6. 담기 후 상태 변화 확인 (버튼이 "취소"로 바뀌거나 카드 스타일 변경) - await page.waitForTimeout(1_000); // API 호출 대기 - - const cancelButton = page.getByRole("button", { name: /취소/ }).first(); - const hasCancelButton = await cancelButton - .isVisible() - .catch(() => false); - - // 담기 성공: 취소 버튼이 나타남 - if (hasCancelButton) { - console.log("SUCCESS: 담기 성공 - 취소 버튼 표시됨"); - } else { - console.log( - "INFO: 취소 버튼이 안 보임 - 담기 결과 추가 확인 필요" - ); - } - - await page.screenshot({ - path: "tests/e2e/screenshots/07-after-add-to-cart.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 8: 장바구니 취소 동작 - // ------------------------------------------- - test("담기 후 취소 버튼으로 장바구니에서 제거된다", async ({ page }) => { - await navigateToPopScreen(page); - - // 거래처 선택 - const selected = await selectTestCustomer(page); - if (!selected) { - test.skip(true, "테스트 거래처를 선택할 수 없음"); - return; - } - - // 이미 담긴 항목이 있는지 확인 (취소 버튼 존재 여부) - const cancelButton = page.getByRole("button", { name: /취소/ }).first(); - const hasCancelButton = await cancelButton - .isVisible({ timeout: 3_000 }) - .catch(() => false); - - if (!hasCancelButton) { - // 먼저 담기를 수행 - const addButton = page.getByRole("button", { name: /담기/ }).first(); - const hasAddButton = await addButton - .isVisible({ timeout: 3_000 }) - .catch(() => false); - if (!hasAddButton) { - test.skip(true, "테스트 데이터 없음 - 취소 테스트 스킵"); - return; - } - await addButton.click(); - await page.waitForTimeout(1_000); - } - - // 취소 버튼 클릭 - const cancelBtn = page.getByRole("button", { name: /취소/ }).first(); - if (await cancelBtn.isVisible()) { - await cancelBtn.click(); - await page.waitForTimeout(1_000); - - // 취소 후: 담기 버튼이 다시 나타남 - const addButtonAfter = page - .getByRole("button", { name: /담기/ }) - .first(); - const hasAddAfterCancel = await addButtonAfter - .isVisible() - .catch(() => false); - - if (hasAddAfterCancel) { - console.log("SUCCESS: 취소 성공 - 담기 버튼 다시 표시됨"); - } - } - - await page.screenshot({ - path: "tests/e2e/screenshots/08-after-cancel.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 9: 반응형 모드 확인 (태블릿 가로) - // ------------------------------------------- - test("태블릿 가로 모드에서 정상 렌더링된다", async ({ page }) => { - // 태블릿 가로 (1024x800) - await page.setViewportSize({ width: 1024, height: 800 }); - await navigateToPopScreen(page); - - await page.waitForTimeout(1_000); - - await page.screenshot({ - path: "tests/e2e/screenshots/09-tablet-landscape.png", - fullPage: true, - }); - }); - - // ------------------------------------------- - // 시나리오 10: 반응형 모드 확인 (모바일 세로) - // ------------------------------------------- - test("모바일 세로 모드에서 정상 렌더링된다", async ({ page }) => { - // 모바일 세로 (375x667) - await page.setViewportSize({ width: 375, height: 667 }); - await navigateToPopScreen(page); - - await page.waitForTimeout(1_000); - - // 화면이 깨지지 않고 렌더링되는지 확인 - const errorText = page.getByText("화면을 찾을 수 없습니다"); - await expect(errorText).not.toBeVisible(); - - await page.screenshot({ - path: "tests/e2e/screenshots/10-mobile-portrait.png", - fullPage: true, - }); - }); -});