import { chromium, Page, Browser } from "playwright"; import * as fs from "fs"; import * as path from "path"; const BASE_URL = "http://localhost:9771"; const API_URL = "http://localhost:8080/api"; const OUTPUT_DIR = path.join(__dirname); interface ComponentTestResult { componentType: string; screenId: number; status: "pass" | "fail" | "no_panel" | "not_found" | "error"; errorMessage?: string; consoleErrors: string[]; hasConfigPanel: boolean; screenshot?: string; } const COMPONENT_SCREEN_MAP: Record = { "v2-input": 60, "v2-select": 71, "v2-date": 77, "v2-button-primary": 47, "v2-text-display": 114, "v2-table-list": 47, "v2-table-search-widget": 79, "v2-media": 74, "v2-split-panel-layout": 74, "v2-tabs-widget": 1011, "v2-section-card": 1188, "v2-section-paper": 202, "v2-card-display": 83, "v2-numbering-rule": 130, "v2-repeater": 1188, "v2-divider-line": 1195, "v2-location-swap-selector": 1195, "v2-category-manager": 135, "v2-file-upload": 138, "v2-pivot-grid": 2327, "v2-rack-structure": 1575, "v2-repeat-container": 2403, "v2-split-line": 4151, "v2-bom-item-editor": 4154, "v2-process-work-standard": 4158, "v2-aggregation-widget": 4119, "flow-widget": 77, "entity-search-input": 3986, "select-basic": 4470, "textarea-basic": 3986, "selected-items-detail-input": 227, "screen-split-panel": 1674, "split-panel-layout2": 2089, "universal-form-modal": 2180, "v2-table-grouped": 79, "v2-status-count": 4498, "v2-timeline-scheduler": 79, }; async function login(page: Page): Promise { await page.goto(`${BASE_URL}/login`); await page.waitForLoadState("networkidle"); await page.getByPlaceholder("사용자 ID를 입력하세요").fill("wace"); await page.getByPlaceholder("비밀번호를 입력하세요").fill("qlalfqjsgh11"); await page.getByRole("button", { name: "로그인" }).click(); await page.waitForTimeout(5000); const token = await page.evaluate(() => localStorage.getItem("authToken") || ""); console.log("Login token obtained:", token ? "YES" : "NO"); return token; } async function openDesigner(page: Page, screenId: number): Promise { await page.evaluate(() => { sessionStorage.setItem( "erp-tab-store", JSON.stringify({ state: { tabs: [ { id: "tab-screenmng", title: "화면 관리", path: "/admin/screenMng/screenMngList", isActive: true, isPinned: false, }, ], activeTabId: "tab-screenmng", }, version: 0, }) ); }); await page.goto(`${BASE_URL}/admin/screenMng/screenMngList?openDesigner=${screenId}`); await page.waitForTimeout(8000); const designerOpen = await page.locator('[class*="designer"], [class*="canvas"], [data-testid*="designer"]').count(); const hasComponents = await page.locator('[data-component-id], [class*="component-wrapper"]').count(); console.log(` Designer elements: ${designerOpen}, Components: ${hasComponents}`); return designerOpen > 0 || hasComponents > 0; } async function testComponentConfigPanel( page: Page, componentType: string, screenId: number ): Promise { const result: ComponentTestResult = { componentType, screenId, status: "error", consoleErrors: [], hasConfigPanel: false, }; const consoleErrors: string[] = []; const pageErrors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { consoleErrors.push(msg.text()); } }); page.on("pageerror", (err) => { pageErrors.push(err.message); }); try { const opened = await openDesigner(page, screenId); if (!opened) { result.status = "error"; result.errorMessage = "Designer failed to open"; return result; } // find component by its url or type attribute in the DOM const componentUrl = componentType.startsWith("v2-") ? `@/lib/registry/components/${componentType}` : `@/lib/registry/components/${componentType}`; // Try clicking on a component of this type // The screen designer renders components with data attributes const componentSelector = `[data-component-type="${componentType}"], [data-component-url*="${componentType}"]`; const componentCount = await page.locator(componentSelector).count(); if (componentCount === 0) { // Try alternative: look for components in the canvas by clicking around // First try to find any clickable component wrapper const wrappers = page.locator('[data-component-id]'); const wrapperCount = await wrappers.count(); if (wrapperCount === 0) { result.status = "not_found"; result.errorMessage = `No components found in screen ${screenId}`; return result; } // Click the first component to see if panel opens let foundTarget = false; for (let i = 0; i < Math.min(wrapperCount, 20); i++) { try { await wrappers.nth(i).click({ force: true, timeout: 2000 }); await page.waitForTimeout(1000); // Check if the properties panel shows the right component type const panelText = await page.locator('[class*="properties"], [class*="config-panel"], [class*="setting"]').textContent().catch(() => ""); if (panelText && panelText.includes(componentType)) { foundTarget = true; break; } } catch { continue; } } if (!foundTarget) { result.status = "not_found"; result.errorMessage = `Component type "${componentType}" not clickable in screen ${screenId}`; } } else { await page.locator(componentSelector).first().click({ force: true }); await page.waitForTimeout(2000); } // Check for the config panel in the right sidebar await page.waitForTimeout(2000); // Look for config panel indicators const configPanelVisible = await page.evaluate(() => { // Check for error boundaries or error messages const errorElements = document.querySelectorAll('[class*="error"], [class*="Error"]'); const errorTexts: string[] = []; errorElements.forEach((el) => { const text = el.textContent || ""; if (text.includes("로드 실패") || text.includes("에러") || text.includes("Error") || text.includes("Cannot read")) { errorTexts.push(text.substring(0, 200)); } }); // Check for config panel elements const hasTabs = document.querySelectorAll('button[role="tab"]').length > 0; const hasLabels = document.querySelectorAll("label").length > 0; const hasInputs = document.querySelectorAll('input, select, [role="combobox"]').length > 0; const hasConfigContent = document.querySelectorAll('[class*="config"], [class*="panel"], [class*="properties"]').length > 0; const hasEditTab = Array.from(document.querySelectorAll("button")).some((b) => b.textContent?.includes("편집")); return { errorTexts, hasTabs, hasLabels, hasInputs, hasConfigContent, hasEditTab, }; }); result.hasConfigPanel = configPanelVisible.hasConfigContent || configPanelVisible.hasEditTab; // Take screenshot const screenshotName = `${componentType.replace(/[^a-zA-Z0-9-]/g, "_")}.png`; const screenshotPath = path.join(OUTPUT_DIR, screenshotName); await page.screenshot({ path: screenshotPath, fullPage: false }); result.screenshot = screenshotName; // Collect errors result.consoleErrors = [...consoleErrors, ...pageErrors]; if (configPanelVisible.errorTexts.length > 0) { result.status = "fail"; result.errorMessage = configPanelVisible.errorTexts.join("; "); } else if (pageErrors.length > 0) { result.status = "fail"; result.errorMessage = pageErrors.join("; "); } else if (consoleErrors.some((e) => e.includes("Cannot read") || e.includes("is not a function") || e.includes("undefined"))) { result.status = "fail"; result.errorMessage = consoleErrors.filter((e) => e.includes("Cannot read") || e.includes("is not a function")).join("; "); } else { result.status = "pass"; } } catch (err: any) { result.status = "error"; result.errorMessage = err.message; } return result; } async function main() { console.log("=== Config Panel Full Audit ==="); console.log(`Testing ${Object.keys(COMPONENT_SCREEN_MAP).length} component types\n`); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // Login const token = await login(page); if (!token) { console.error("Login failed!"); await browser.close(); return; } const results: ComponentTestResult[] = []; const componentTypes = Object.keys(COMPONENT_SCREEN_MAP); for (let i = 0; i < componentTypes.length; i++) { const componentType = componentTypes[i]; const screenId = COMPONENT_SCREEN_MAP[componentType]; console.log(`\n[${i + 1}/${componentTypes.length}] Testing: ${componentType} (screen: ${screenId})`); const result = await testComponentConfigPanel(page, componentType, screenId); results.push(result); const statusEmoji = { pass: "OK", fail: "FAIL", no_panel: "NO_PANEL", not_found: "NOT_FOUND", error: "ERROR", }[result.status]; console.log(` Result: ${statusEmoji} ${result.errorMessage || ""}`); // Clear console listeners for next iteration page.removeAllListeners("console"); page.removeAllListeners("pageerror"); } await browser.close(); // Write results const reportPath = path.join(OUTPUT_DIR, "audit-results.json"); fs.writeFileSync(reportPath, JSON.stringify(results, null, 2)); // Summary console.log("\n\n=== AUDIT SUMMARY ==="); const passed = results.filter((r) => r.status === "pass"); const failed = results.filter((r) => r.status === "fail"); const errors = results.filter((r) => r.status === "error"); const notFound = results.filter((r) => r.status === "not_found"); console.log(`PASS: ${passed.length}`); console.log(`FAIL: ${failed.length}`); console.log(`ERROR: ${errors.length}`); console.log(`NOT_FOUND: ${notFound.length}`); if (failed.length > 0) { console.log("\n--- FAILED Components ---"); failed.forEach((r) => { console.log(` ${r.componentType} (screen: ${r.screenId}): ${r.errorMessage}`); }); } if (errors.length > 0) { console.log("\n--- ERROR Components ---"); errors.forEach((r) => { console.log(` ${r.componentType} (screen: ${r.screenId}): ${r.errorMessage}`); }); } // Write markdown report const mdReport = generateMarkdownReport(results); fs.writeFileSync(path.join(OUTPUT_DIR, "audit-report.md"), mdReport); console.log(`\nReport saved to ${OUTPUT_DIR}/audit-report.md`); } function generateMarkdownReport(results: ComponentTestResult[]): string { const lines: string[] = []; lines.push("# Config Panel Audit Report"); lines.push(`\nDate: ${new Date().toISOString()}`); lines.push(`\nTotal: ${results.length} components tested\n`); const passed = results.filter((r) => r.status === "pass"); const failed = results.filter((r) => r.status === "fail"); const errors = results.filter((r) => r.status === "error"); const notFound = results.filter((r) => r.status === "not_found"); lines.push(`| Status | Count |`); lines.push(`|--------|-------|`); lines.push(`| PASS | ${passed.length} |`); lines.push(`| FAIL | ${failed.length} |`); lines.push(`| ERROR | ${errors.length} |`); lines.push(`| NOT_FOUND | ${notFound.length} |`); lines.push(`\n## Failed Components\n`); if (failed.length === 0) { lines.push("None\n"); } else { lines.push(`| Component | Screen ID | Error |`); lines.push(`|-----------|-----------|-------|`); failed.forEach((r) => { lines.push(`| ${r.componentType} | ${r.screenId} | ${(r.errorMessage || "").substring(0, 100)} |`); }); } lines.push(`\n## Error Components\n`); if (errors.length === 0) { lines.push("None\n"); } else { lines.push(`| Component | Screen ID | Error |`); lines.push(`|-----------|-----------|-------|`); errors.forEach((r) => { lines.push(`| ${r.componentType} | ${r.screenId} | ${(r.errorMessage || "").substring(0, 100)} |`); }); } lines.push(`\n## Not Found Components\n`); if (notFound.length === 0) { lines.push("None\n"); } else { notFound.forEach((r) => { lines.push(`- ${r.componentType} (screen: ${r.screenId}): ${r.errorMessage}`); }); } lines.push(`\n## All Results\n`); lines.push(`| # | Component | Screen | Status | Config Panel | Error |`); lines.push(`|---|-----------|--------|--------|--------------|-------|`); results.forEach((r, i) => { const status = r.status.toUpperCase(); lines.push( `| ${i + 1} | ${r.componentType} | ${r.screenId} | ${status} | ${r.hasConfigPanel ? "Yes" : "No"} | ${(r.errorMessage || "-").substring(0, 80)} |` ); }); return lines.join("\n"); } main().catch(console.error);