feat: Enhance SelectedItemsDetailInputComponent with sourceKeyField auto-detection and FK mapping
- Implemented automatic detection of sourceKeyField based on component configuration, improving flexibility in data handling. - Enhanced the SelectedItemsDetailInputConfigPanel to support automatic FK detection and mapping, streamlining the configuration process. - Updated the database connection logic to handle DATE types correctly, preventing timezone-related issues. - Improved overall component performance by optimizing memoization and state management for better user experience.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 구매관리 - 공급업체관리 / 구매품목정보 CRUD 브라우저 테스트
|
||||
* 실행: node scripts/browser-test-purchase-supplier.js
|
||||
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-purchase-supplier.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = "test-screenshots";
|
||||
const CREDENTIALS = { userId: "topseal_admin", password: "qlalfqjsgh11" };
|
||||
|
||||
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();
|
||||
|
||||
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}`);
|
||||
return path;
|
||||
};
|
||||
|
||||
const clickMenu = async (text) => {
|
||||
const loc = page.getByText(text, { exact: true }).first();
|
||||
if ((await loc.count()) > 0) {
|
||||
await loc.click();
|
||||
return true;
|
||||
}
|
||||
const alt = page.getByRole("link", { name: text }).or(page.locator(`a:has-text("${text}")`)).first();
|
||||
if ((await alt.count()) > 0) {
|
||||
await alt.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const clickRow = async () => {
|
||||
const rows = await page.$$('tbody tr, table tr, [role="row"]');
|
||||
for (const r of rows) {
|
||||
const t = await r.textContent();
|
||||
if (t && !t.includes("데이터가 없습니다") && !t.includes("로딩")) {
|
||||
await r.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (rows.length > 0) {
|
||||
await rows[0].click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const clickButton = async (regex) => {
|
||||
const btn = page.locator("button").filter({ hasText: regex }).first();
|
||||
try {
|
||||
if ((await btn.count()) > 0 && !(await btn.isDisabled())) {
|
||||
await btn.click();
|
||||
return true;
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
try {
|
||||
console.log("\n=== 로그인 확인 ===\n");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", CREDENTIALS.userId);
|
||||
await page.fill("#password", CREDENTIALS.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
results.success.push("세션 확인");
|
||||
|
||||
// ========== 테스트 1: 공급업체관리 ==========
|
||||
console.log("\n=== 테스트 1: 공급업체관리 ===\n");
|
||||
|
||||
console.log("단계 1: 구매관리 메뉴 열기");
|
||||
if (await clickMenu("구매관리")) {
|
||||
await page.waitForTimeout(3000);
|
||||
results.success.push("구매관리 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("구매관리 메뉴 미발견");
|
||||
}
|
||||
await screenshot("p1_01_purchase_menu");
|
||||
|
||||
console.log("단계 2: 공급업체관리 서브메뉴 클릭");
|
||||
if (await clickMenu("공급업체관리")) {
|
||||
await page.waitForTimeout(8000);
|
||||
results.success.push("공급업체관리 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("공급업체관리 메뉴 미발견");
|
||||
}
|
||||
await screenshot("p1_02_supplier_screen");
|
||||
|
||||
console.log("단계 3: 공급업체 선택");
|
||||
if (await clickRow()) {
|
||||
await page.waitForTimeout(5000);
|
||||
results.success.push("공급업체 행 클릭");
|
||||
} else {
|
||||
results.failed.push("공급업체 테이블 행 미발견");
|
||||
}
|
||||
await screenshot("p1_03_after_supplier_select");
|
||||
|
||||
console.log("단계 4: 납품품목 탭/영역 확인");
|
||||
const itemTab = page.getByText(/납품품목|품목/).first();
|
||||
if ((await itemTab.count()) > 0) {
|
||||
await itemTab.click();
|
||||
await page.waitForTimeout(3000);
|
||||
results.success.push("납품품목/품목 탭 클릭");
|
||||
} else {
|
||||
results.failed.push("납품품목 탭 미발견");
|
||||
}
|
||||
await screenshot("p1_04_item_tab");
|
||||
|
||||
console.log("단계 5: 품목 추가 시도");
|
||||
const addBtn = page.locator("button").filter({ hasText: /추가|\+ 추가/ }).first();
|
||||
let addBtnEnabled = false;
|
||||
try {
|
||||
addBtnEnabled = (await addBtn.count()) > 0 && !(await addBtn.isDisabled());
|
||||
} catch (_) {}
|
||||
if (addBtnEnabled) {
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(2000);
|
||||
const modal = await page.$('[role="dialog"], .modal, [class*="modal"]');
|
||||
if (modal) {
|
||||
const modalRow = await page.$('[role="dialog"] tbody tr, .modal tbody tr');
|
||||
if (modalRow) {
|
||||
await modalRow.click();
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
results.success.push("추가 버튼 클릭 및 품목 선택 시도");
|
||||
} else {
|
||||
results.failed.push("추가 버튼 미발견 또는 비활성화");
|
||||
}
|
||||
await screenshot("p1_05_add_item");
|
||||
|
||||
// ========== 테스트 2: 구매품목정보 ==========
|
||||
console.log("\n=== 테스트 2: 구매품목정보 ===\n");
|
||||
|
||||
console.log("단계 6: 구매품목정보 메뉴 클릭");
|
||||
if (await clickMenu("구매품목정보")) {
|
||||
await page.waitForTimeout(8000);
|
||||
results.success.push("구매품목정보 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("구매품목정보 메뉴 미발견");
|
||||
}
|
||||
await screenshot("p2_01_item_screen");
|
||||
|
||||
console.log("단계 7: 품목 선택 및 공급업체 확인");
|
||||
if (await clickRow()) {
|
||||
await page.waitForTimeout(5000);
|
||||
results.success.push("구매품목 행 클릭");
|
||||
} else {
|
||||
results.failed.push("구매품목 테이블 행 미발견");
|
||||
}
|
||||
await screenshot("p2_02_after_item_select");
|
||||
|
||||
// SelectedItemsDetailInput 컴포넌트 확인
|
||||
const hasDetailInput = await page.$('input[placeholder*="품번"], [class*="selected-items"], input[name*="품번"]');
|
||||
results.success.push(hasDetailInput ? "SelectedItemsDetailInput 렌더링 확인" : "SelectedItemsDetailInput 미확인");
|
||||
await screenshot("p2_03_final");
|
||||
|
||||
console.log("\n========== 테스트 결과 ==========\n");
|
||||
console.log("성공:", results.success);
|
||||
console.log("실패:", results.failed);
|
||||
console.log("스크린샷:", results.screenshots);
|
||||
|
||||
} 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 (_) {}
|
||||
console.error(err);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
runTest()
|
||||
.then((r) => process.exit(r.failed.length > 0 ? 1 : 0))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user