288 lines
9.9 KiB
TypeScript
288 lines
9.9 KiB
TypeScript
|
|
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<any[]> {
|
||
|
|
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<number, string[]>();
|
||
|
|
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);
|