chore: Playwright 테스트 파일을 관제탑(My-agent)으로 이관

테스트 파일은 ERP-node 프로젝트가 아닌 My-agent에서 관리한다.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
SeongHyun Kim 2026-03-24 19:23:55 +09:00
parent 2da1532e65
commit b677840952
2 changed files with 0 additions and 571 deletions

View File

@ -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 },
},
},
],
});

View File

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