import { chromium, Page } from "playwright"; import * as fs from "fs"; import * as path from "path"; const BASE = "http://localhost:9771"; const OUT = path.join(__dirname); const TARGETS: [string, number][] = [ ["v2-input", 60], ["v2-select", 71], ["v2-date", 77], ["v2-button-primary", 50], ["v2-text-display", 114], ["v2-table-list", 68], ["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], ]; interface Result { type: string; screenId: number; compId: string; status: "PASS" | "FAIL" | "ERROR" | "NO_COMPONENT"; jsErrors: string[]; panelDetails: string; errorMsg?: string; } async function login(page: Page) { await page.goto(`${BASE}/login`); await page.waitForLoadState("networkidle"); await page.waitForTimeout(2000); await page.fill('[placeholder="사용자 ID를 입력하세요"]', "wace"); await page.fill('[placeholder="비밀번호를 입력하세요"]', "qlalfqjsgh11"); await Promise.all([ page.waitForResponse((r) => r.url().includes("/auth/login"), { timeout: 15000 }).catch(() => null), page.click('button:has-text("로그인")'), ]); await page.waitForTimeout(5000); const hasToken = await page.evaluate(() => !!localStorage.getItem("authToken")); console.log("Login:", hasToken ? "OK" : "FAILED"); return hasToken; } async function getLayoutComponents(page: Page, screenId: number): Promise { return page.evaluate(async (sid) => { const token = localStorage.getItem("authToken") || ""; const host = window.location.hostname; const apiBase = host === "localhost" || host === "127.0.0.1" ? "http://localhost:8080/api" : "/api"; try { const resp = await fetch(`${apiBase}/screen-management/screens/${sid}/layout-v2`, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, }); const data = await resp.json(); if (data.success && data.data?.components) return data.data.components; if (data.success && Array.isArray(data.data)) { const all: any[] = []; for (const layer of data.data) { if (layer.layout_data?.components) all.push(...layer.layout_data.components); } return all; } return []; } catch { return []; } }, screenId); } function findCompId(components: any[], targetType: string): string { for (const c of components) { const url: string = c.url || ""; const ctype: string = c.componentType || ""; if (url.endsWith("/" + targetType) || ctype === targetType) return c.id; } return ""; } async function openDesigner(page: Page, screenId: number) { await page.evaluate(() => { sessionStorage.setItem("erp-tab-store", JSON.stringify({ state: { tabs: [{ id: "tab-sm", title: "화면 관리", path: "/admin/screenMng/screenMngList", isActive: true, isPinned: false }], activeTabId: "tab-sm", }, version: 0, })); }); await page.goto(`${BASE}/admin/screenMng/screenMngList?openDesigner=${screenId}`, { timeout: 60000, waitUntil: "domcontentloaded", }); await page.waitForTimeout(10000); } async function checkPanel(page: Page): Promise<{ visible: boolean; hasError: boolean; detail: string }> { return page.evaluate(() => { const body = document.body.innerText || ""; const hasError = body.includes("로드 실패") || body.includes("Cannot read properties"); const labels = document.querySelectorAll("label").length; const inputs = document.querySelectorAll('input:not([type="hidden"])').length; const selects = document.querySelectorAll('select, [role="combobox"], [role="listbox"]').length; const tabs = document.querySelectorAll('[role="tab"]').length; const switches = document.querySelectorAll('[role="switch"]').length; const total = labels + inputs + selects + tabs + switches; const detail = `L=${labels} I=${inputs} S=${selects} T=${tabs} SW=${switches}`; return { visible: total > 3, hasError, detail }; }); } async function main() { console.log("=== Config Panel Audit v3 ===\n"); const browser = await chromium.launch({ headless: true }); const ctx = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); const page = await ctx.newPage(); const jsErrors: string[] = []; page.on("pageerror", (err) => jsErrors.push(err.message.substring(0, 300))); const ok = await login(page); if (!ok) { await browser.close(); return; } const groups = new Map(); for (const [type, sid] of TARGETS) { if (!groups.has(sid)) groups.set(sid, []); groups.get(sid)!.push(type); } const allResults: Result[] = []; const entries = Array.from(groups.entries()); for (let i = 0; i < entries.length; i++) { const [screenId, types] = entries[i]; console.log(`\n[${i + 1}/${entries.length}] Screen ${screenId}: ${types.join(", ")}`); const components = await getLayoutComponents(page, screenId); console.log(` API: ${components.length} comps`); await openDesigner(page, screenId); const domCompCount = await page.locator('[data-component-id]').count(); console.log(` DOM: ${domCompCount} comp wrappers`); for (const targetType of types) { const errIdx = jsErrors.length; const compId = findCompId(components, targetType); if (!compId) { console.log(` ${targetType}: NO_COMPONENT`); allResults.push({ type: targetType, screenId, compId: "", status: "NO_COMPONENT", jsErrors: [], panelDetails: "", errorMsg: "Not in layout" }); continue; } // 컴포넌트 클릭 let clicked = false; const sel = `[data-component-id="${compId}"], #component-${compId}`; const elCount = await page.locator(sel).count(); if (elCount > 0) { try { await page.locator(sel).first().click({ force: true, timeout: 5000 }); await page.waitForTimeout(2000); clicked = true; } catch { try { await page.locator(sel).first().dispatchEvent("click"); await page.waitForTimeout(2000); clicked = true; } catch {} } } if (!clicked) { // fallback: 캔버스에서 위치 기반 클릭 시도 const comp = components.find((c: any) => c.id === compId); if (comp?.position) { const canvasEl = await page.locator('[class*="canvas"], [class*="designer"]').first().boundingBox(); if (canvasEl) { const x = canvasEl.x + (comp.position.x || 100); const y = canvasEl.y + (comp.position.y || 100); await page.mouse.click(x, y); await page.waitForTimeout(2000); clicked = true; } } } if (!clicked) { console.log(` ${targetType}: ERROR (click fail, id=${compId})`); allResults.push({ type: targetType, screenId, compId, status: "ERROR", jsErrors: jsErrors.slice(errIdx), panelDetails: "", errorMsg: "Cannot click component" }); continue; } const panel = await checkPanel(page); const newErrors = jsErrors.slice(errIdx); const critical = newErrors.find((e) => e.includes("Cannot read") || e.includes("is not a function") || e.includes("is not defined") || e.includes("Minified React") ); let status: Result["status"]; let errorMsg: string | undefined; if (panel.hasError || critical) { status = "FAIL"; errorMsg = critical || "Panel error"; } else if (!panel.visible) { status = "FAIL"; errorMsg = `Panel not visible (${panel.detail})`; } else { status = "PASS"; } const icon = { PASS: "OK", FAIL: "FAIL", ERROR: "ERR", NO_COMPONENT: "??" }[status]; console.log(` ${targetType}: ${icon} [${panel.detail}]${errorMsg ? " " + errorMsg.substring(0, 80) : ""}`); if (status === "FAIL") { await page.screenshot({ path: path.join(OUT, `fail-${targetType.replace(/\//g, "_")}.png`) }).catch(() => {}); } allResults.push({ type: targetType, screenId, compId, status, jsErrors: newErrors, panelDetails: panel.detail, errorMsg }); } } await browser.close(); fs.writeFileSync(path.join(OUT, "results.json"), JSON.stringify(allResults, null, 2)); console.log("\n\n=== AUDIT SUMMARY ==="); const p = allResults.filter((r) => r.status === "PASS"); const f = allResults.filter((r) => r.status === "FAIL"); const e = allResults.filter((r) => r.status === "ERROR"); const n = allResults.filter((r) => r.status === "NO_COMPONENT"); console.log(`Total: ${allResults.length}`); console.log(`PASS: ${p.length}`); console.log(`FAIL: ${f.length}`); console.log(`ERROR: ${e.length}`); console.log(`NO_COMPONENT: ${n.length}`); if (f.length > 0) { console.log("\n--- FAILED ---"); f.forEach((r) => console.log(` ${r.type} (screen ${r.screenId}): ${r.errorMsg}`)); } if (e.length > 0) { console.log("\n--- ERRORS ---"); e.forEach((r) => console.log(` ${r.type} (screen ${r.screenId}): ${r.errorMsg}`)); } const unique = [...new Set(jsErrors)]; if (unique.length > 0) { console.log(`\n--- JS Errors (${unique.length} unique) ---`); unique.slice(0, 15).forEach((err) => console.log(` ${err.substring(0, 150)}`)); } console.log("\nDone."); } main().catch(console.error);