390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
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<string, number> = {
|
|
"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<string> {
|
|
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<boolean> {
|
|
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<ComponentTestResult> {
|
|
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);
|