#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Quant Engine UI Completeness Test Playwright를 사용한 자동화 DOM 분석 및 완성도 평가 """ import asyncio import json import sys import os from datetime import datetime from pathlib import Path if sys.platform == "win32": import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') from playwright.async_api import async_playwright, Page BASE_URL = "http://localhost:5265" class UIAnalyzer: """MudBlazor UI 완성도 분석""" def __init__(self): self.results = { "timestamp": datetime.now().isoformat(), "base_url": BASE_URL, "tests": {}, "score": 0, "issues": [], "recommendations": [] } async def run_all_tests(self): """모든 테스트 실행""" async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page() try: # 1. 페이지 로딩 테스트 await self.test_page_load(page) # 2. MudBlazor 요소 검증 await self.test_mudblazor_components(page) # 3. 레이아웃 검증 await self.test_layout_structure(page) # 4. Dashboard 콘텐츠 검증 await self.test_dashboard_content(page) # 5. 네비게이션 검증 await self.test_navigation(page) # 6. 반응형 디자인 검증 await self.test_responsive_design(page) # 7. 접근성 검증 await self.test_accessibility(page) # 8. 성능 메트릭 수집 await self.test_performance(page) finally: await browser.close() # 점수 계산 및 리포트 생성 self.calculate_score() return self.results async def test_page_load(self, page: Page): """페이지 로드 테스트""" test_name = "page_load" self.results["tests"][test_name] = {"status": "PENDING", "checks": []} try: response = await page.goto(BASE_URL, wait_until="networkidle") status_ok = response.status == 200 self.results["tests"][test_name]["checks"].append({ "name": "HTTP Status 200", "passed": status_ok, "value": response.status }) # 타이틀 확인 title = await page.title() title_ok = "Dashboard" in title or "Quant Engine" in title self.results["tests"][test_name]["checks"].append({ "name": "Page Title", "passed": title_ok, "value": title }) # 로드 시간 metrics = await page.evaluate("() => window.performance.timing") load_time = metrics.get("loadEventEnd", 0) - metrics.get("navigationStart", 0) load_ok = load_time < 5000 # 5초 이내 self.results["tests"][test_name]["checks"].append({ "name": "Load Time < 5s", "passed": load_ok, "value": f"{load_time}ms" }) self.results["tests"][test_name]["status"] = "PASS" if all( c["passed"] for c in self.results["tests"][test_name]["checks"] ) else "FAIL" except Exception as e: self.results["tests"][test_name]["status"] = "ERROR" self.results["issues"].append(f"Page Load Error: {str(e)}") async def test_mudblazor_components(self, page: Page): """MudBlazor 컴포넌트 검증""" test_name = "mudblazor_components" self.results["tests"][test_name] = {"status": "PENDING", "components": []} components = { "MudLayout": "div.mud-layout", "MudAppBar": "header.mud-appbar", "MudDrawer": "aside.mud-drawer", "MudMainContent": "main.mud-main-content", "MudCard": "div.mud-card", "MudText": "p.mud-typography", "MudButton": "button", "MudIcon": "svg.mud-icon-root" } for component_name, selector in components.items(): try: count = await page.locator(selector).count() found = count > 0 self.results["tests"][test_name]["components"].append({ "name": component_name, "selector": selector, "found": found, "count": count }) except Exception as e: self.results["tests"][test_name]["components"].append({ "name": component_name, "selector": selector, "found": False, "error": str(e) }) found_count = sum(1 for c in self.results["tests"][test_name]["components"] if c["found"]) self.results["tests"][test_name]["status"] = "PASS" if found_count >= 4 else "FAIL" async def test_layout_structure(self, page: Page): """레이아웃 구조 검증""" test_name = "layout_structure" self.results["tests"][test_name] = {"status": "PENDING", "checks": []} # 1. MudLayout 존재 layout_exists = await page.locator("div.mud-layout").count() > 0 self.results["tests"][test_name]["checks"].append({ "name": "MudLayout exists", "passed": layout_exists }) # 2. AppBar 존재 appbar_exists = await page.locator("header.mud-appbar").count() > 0 self.results["tests"][test_name]["checks"].append({ "name": "MudAppBar exists", "passed": appbar_exists }) # 3. Drawer 존재 drawer_exists = await page.locator("aside.mud-drawer").count() > 0 self.results["tests"][test_name]["checks"].append({ "name": "MudDrawer exists", "passed": drawer_exists }) # 4. MainContent 존재 main_exists = await page.locator("main.mud-main-content").count() > 0 self.results["tests"][test_name]["checks"].append({ "name": "MudMainContent exists", "passed": main_exists }) # 5. MudText 최소 3개 (헤더 등) text_count = await page.locator("p.mud-typography, h1, h2, h3, h4, h5, h6").count() text_ok = text_count >= 3 self.results["tests"][test_name]["checks"].append({ "name": f"Text elements >= 3 (found {text_count})", "passed": text_ok }) self.results["tests"][test_name]["status"] = "PASS" if all( c["passed"] for c in self.results["tests"][test_name]["checks"] ) else "FAIL" async def test_dashboard_content(self, page: Page): """Dashboard 콘텐츠 검증""" test_name = "dashboard_content" self.results["tests"][test_name] = {"status": "PENDING", "elements": []} elements = { "Dashboard Title": "text=Dashboard", "Status Card": "text=Status", "Active Locks": "text=Active Locks", "System Info": "text=System Information", "Connected Badge": "text=Connected" } for elem_name, selector in elements.items(): try: found = await page.locator(f"text={selector.replace('text=', '')}").count() > 0 self.results["tests"][test_name]["elements"].append({ "name": elem_name, "found": found }) except: self.results["tests"][test_name]["elements"].append({ "name": elem_name, "found": False }) found_count = sum(1 for e in self.results["tests"][test_name]["elements"] if e["found"]) self.results["tests"][test_name]["status"] = "PASS" if found_count >= 3 else "FAIL" async def test_navigation(self, page: Page): """네비게이션 검증""" test_name = "navigation" self.results["tests"][test_name] = {"status": "PENDING", "nav_items": []} nav_items = { "Dashboard": "text=Dashboard", "Portfolio": "text=Portfolio", "Analytics": "text=Analytics", "Reports": "text=Reports", "Settings": "text=Settings" } for item_name, selector in nav_items.items(): try: found = await page.locator(selector).count() > 0 self.results["tests"][test_name]["nav_items"].append({ "name": item_name, "found": found }) except: self.results["tests"][test_name]["nav_items"].append({ "name": item_name, "found": False }) found_count = sum(1 for n in self.results["tests"][test_name]["nav_items"] if n["found"]) self.results["tests"][test_name]["status"] = "PASS" if found_count >= 3 else "FAIL" async def test_responsive_design(self, page: Page): """반응형 디자인 검증""" test_name = "responsive_design" self.results["tests"][test_name] = {"status": "PENDING", "viewports": []} viewports = [ {"name": "Mobile (375x667)", "width": 375, "height": 667}, {"name": "Tablet (768x1024)", "width": 768, "height": 1024}, {"name": "Desktop (1920x1080)", "width": 1920, "height": 1080} ] for viewport in viewports: await page.set_viewport_size({"width": viewport["width"], "height": viewport["height"]}) # 요소가 여전히 보이는지 확인 visible = await page.locator("header.mud-appbar").is_visible() self.results["tests"][test_name]["viewports"].append({ "name": viewport["name"], "size": f"{viewport['width']}x{viewport['height']}", "appbar_visible": visible }) self.results["tests"][test_name]["status"] = "PASS" if all( v["appbar_visible"] for v in self.results["tests"][test_name]["viewports"] ) else "FAIL" async def test_accessibility(self, page: Page): """접근성 검증 (기본)""" test_name = "accessibility" self.results["tests"][test_name] = {"status": "PENDING", "checks": []} # 1. Lang 속성 html_lang = await page.locator("html").get_attribute("lang") lang_ok = html_lang is not None self.results["tests"][test_name]["checks"].append({ "name": "HTML lang attribute", "passed": lang_ok, "value": html_lang }) # 2. Meta charset charset = await page.locator("meta[charset]").count() > 0 self.results["tests"][test_name]["checks"].append({ "name": "Meta charset", "passed": charset }) # 3. Viewport meta viewport = await page.locator("meta[name='viewport']").count() > 0 self.results["tests"][test_name]["checks"].append({ "name": "Meta viewport", "passed": viewport }) # 4. Heading hierarchy headings = await page.locator("h1, h2, h3, h4, h5, h6").count() heading_ok = headings > 0 self.results["tests"][test_name]["checks"].append({ "name": f"Heading hierarchy (found {headings})", "passed": heading_ok }) self.results["tests"][test_name]["status"] = "PASS" if all( c["passed"] for c in self.results["tests"][test_name]["checks"] ) else "FAIL" async def test_performance(self, page: Page): """성능 메트릭 수집""" test_name = "performance" self.results["tests"][test_name] = {"status": "PASS", "metrics": {}} try: metrics = await page.evaluate(""" () => ({ domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart, resources: performance.getEntriesByType('resource').length, memoryUsage: performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1048576) : null }) """) self.results["tests"][test_name]["metrics"] = { "DOM Content Loaded (ms)": metrics.get("domContentLoaded", 0), "Page Load Complete (ms)": metrics.get("loadComplete", 0), "Resources Loaded": metrics.get("resources", 0), "Memory Usage (MB)": metrics.get("memoryUsage") } except Exception as e: self.results["tests"][test_name]["status"] = "WARN" self.results["tests"][test_name]["error"] = str(e) def calculate_score(self): """완성도 점수 계산""" total_weight = 0 total_score = 0 weights = { "page_load": 15, "mudblazor_components": 20, "layout_structure": 20, "dashboard_content": 15, "navigation": 15, "responsive_design": 10, "accessibility": 5, "performance": 0 # 점수에 포함 안 함, 참고용 } for test_name, weight in weights.items(): if test_name in self.results["tests"]: test = self.results["tests"][test_name] if test["status"] == "PASS": total_score += weight elif test["status"] == "FAIL": # 부분 점수 if "checks" in test: passed = sum(1 for c in test["checks"] if c.get("passed", False)) total = len(test["checks"]) total_score += weight * (passed / total) elif "components" in test: found = sum(1 for c in test["components"] if c.get("found", False)) total = len(test["components"]) total_score += weight * (found / total) total_weight += weight self.results["score"] = round(total_score, 1) if total_weight > 0 else 0 self.results["max_score"] = total_weight # 권장사항 생성 self.generate_recommendations() def generate_recommendations(self): """개선 권장사항 생성""" recommendations = [] # Dashboard 콘텐츠 부족 if self.results["tests"]["dashboard_content"]["status"] == "FAIL": recommendations.append({ "category": "Content", "priority": "HIGH", "issue": "Dashboard 콘텐츠가 부족함", "suggestion": "스타투스 카드, 통계, 실시간 데이터 추가", "files": ["src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor"] }) # 네비게이션 미완성 if self.results["tests"]["navigation"]["status"] == "FAIL": found = sum(1 for n in self.results["tests"]["navigation"]["nav_items"] if n["found"]) recommendations.append({ "category": "Navigation", "priority": "MEDIUM", "issue": f"네비게이션 항목 {found}/5개만 구현됨", "suggestion": "모든 네비게이션 항목 추가 (Analytics, Reports 등)", "files": ["src/dotnet/QuantEngine.Web/Components/Layout/NavMenu.razor"] }) # 반응형 디자인 개선 if self.results["tests"]["responsive_design"]["status"] == "FAIL": recommendations.append({ "category": "UI/UX", "priority": "MEDIUM", "issue": "일부 뷰포트에서 레이아웃 깨짐", "suggestion": "MudContainer MaxWidth 조정, Grid 반응형 설정 확인", "files": ["src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor"] }) # 접근성 개선 if self.results["tests"]["accessibility"]["status"] == "FAIL": recommendations.append({ "category": "Accessibility", "priority": "LOW", "issue": "접근성 표준 미충족", "suggestion": "ARIA 라벨 추가, 색상 대비 개선", "files": ["src/dotnet/QuantEngine.Web/Components/App.razor"] }) self.results["recommendations"] = recommendations async def take_screenshot(self, page: Page, filename: str): """스크린샷 캡처""" await page.screenshot(path=filename) print(f"✓ Screenshot: {filename}") async def main(): """메인 실행""" analyzer = UIAnalyzer() print("🧪 Quant Engine UI Completeness Test") print("=" * 70) print(f"URL: {BASE_URL}") print("=" * 70) try: results = await analyzer.run_all_tests() # 결과 출력 print("\n📊 Test Results") print("-" * 70) for test_name, test_data in results["tests"].items(): status = test_data["status"] status_emoji = "✅" if status == "PASS" else "❌" if status == "FAIL" else "⚠️" print(f"{status_emoji} {test_name.upper()}: {status}") print("\n" + "=" * 70) print(f"📈 Completeness Score: {results['score']}/{results['max_score']} ({results['score']/results['max_score']*100:.1f}%)") print("=" * 70) # 권장사항 if results["recommendations"]: print("\n💡 Recommendations for Improvement") print("-" * 70) for i, rec in enumerate(results["recommendations"], 1): print(f"\n{i}. [{rec['priority']}] {rec['issue']}") print(f" Category: {rec['category']}") print(f" Suggestion: {rec['suggestion']}") print(f" Files: {', '.join(rec['files'])}") # 결과 저장 output_file = Path("Temp/ui_test_results.json") output_file.parent.mkdir(parents=True, exist_ok=True) output_file.write_text(json.dumps(results, indent=2, ensure_ascii=False)) print(f"\n✓ Results saved: {output_file}") return results except Exception as e: print(f"\n❌ Error: {e}") import traceback traceback.print_exc() if __name__ == "__main__": asyncio.run(main())