Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
406
.dss/test_smoke_phase1.py
Normal file
406
.dss/test_smoke_phase1.py
Normal file
@@ -0,0 +1,406 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DSS Admin UI - Phase 1: Smoke Test (Component Loading)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Framework: Pytest-Playwright (Python-based)
|
||||
Purpose: Validate all 51 components load successfully with zero console errors
|
||||
Coverage: Component loading, DOM rendering, error detection
|
||||
|
||||
Test Strategy:
|
||||
1. Navigate to component
|
||||
2. Wait for load (max 3s)
|
||||
3. Check console logs (zero errors expected)
|
||||
4. Validate DOM rendering (not blank)
|
||||
5. Record result
|
||||
|
||||
Generated: 2025-12-08
|
||||
Author: Gemini 3 Pro Expert Analysis
|
||||
Status: Ready for Implementation
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright, expect
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# Component registry from component-registry.js
|
||||
COMPONENTS = {
|
||||
# Tools components
|
||||
'ds-metrics-panel': 'Tools',
|
||||
'ds-console-viewer': 'Tools',
|
||||
'ds-token-inspector': 'Tools',
|
||||
'ds-figma-status': 'Tools',
|
||||
'ds-activity-log': 'Tools',
|
||||
'ds-visual-diff': 'Tools',
|
||||
'ds-accessibility-report': 'Tools',
|
||||
'ds-screenshot-gallery': 'Tools',
|
||||
'ds-network-monitor': 'Tools',
|
||||
'ds-test-results': 'Tools',
|
||||
'ds-system-log': 'Tools',
|
||||
'ds-figma-extract-quick': 'Tools',
|
||||
'ds-quick-wins-script': 'Tools',
|
||||
'ds-regression-testing': 'Tools',
|
||||
'ds-storybook-figma-compare': 'Tools',
|
||||
'ds-storybook-live-compare': 'Tools',
|
||||
'ds-figma-extraction': 'Tools',
|
||||
'ds-project-analysis': 'Tools',
|
||||
'ds-figma-plugin': 'Tools',
|
||||
'ds-token-list': 'Tools',
|
||||
'ds-asset-list': 'Tools',
|
||||
'ds-component-list': 'Tools',
|
||||
'ds-navigation-demos': 'Tools',
|
||||
'ds-figma-live-compare': 'Tools',
|
||||
'ds-esre-editor': 'Tools',
|
||||
'ds-chat-panel': 'Tools',
|
||||
'ds-quick-wins': 'Tools',
|
||||
|
||||
# Layout components
|
||||
'ds-shell': 'Layout',
|
||||
'ds-panel': 'Layout',
|
||||
'ds-activity-bar': 'Layout',
|
||||
'ds-ai-chat-sidebar': 'Layout',
|
||||
'ds-project-selector': 'Layout',
|
||||
|
||||
# Metrics components
|
||||
'ds-frontpage': 'Metrics',
|
||||
'ds-metric-card': 'Metrics',
|
||||
'ds-metrics-dashboard': 'Metrics',
|
||||
|
||||
# Admin components
|
||||
'ds-admin-settings': 'Admin',
|
||||
'ds-project-list': 'Admin',
|
||||
'ds-user-settings': 'Admin',
|
||||
|
||||
# UI Elements
|
||||
'ds-action-bar': 'UI',
|
||||
'ds-badge': 'UI',
|
||||
'ds-base-tool': 'Base',
|
||||
'ds-button': 'UI',
|
||||
'ds-card': 'UI',
|
||||
'ds-component-base': 'Base',
|
||||
'ds-input': 'UI',
|
||||
'ds-notification-center': 'UI',
|
||||
'ds-toast': 'UI',
|
||||
'ds-toast-provider': 'UI',
|
||||
'ds-workflow': 'UI',
|
||||
|
||||
# Listing components
|
||||
'ds-icon-list': 'Listings',
|
||||
'ds-jira-issues': 'Listings',
|
||||
}
|
||||
|
||||
BASE_URL = "http://localhost:5173" # Vite dev server
|
||||
TIMEOUT = 3000 # 3 seconds for component load
|
||||
|
||||
|
||||
class ConsoleLogCapture:
|
||||
"""Capture and analyze console logs during test"""
|
||||
def __init__(self):
|
||||
self.logs = []
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
|
||||
def handle_console_message(self, msg):
|
||||
"""Handle console events from browser"""
|
||||
entry = {
|
||||
'type': msg.type,
|
||||
'text': msg.text,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
self.logs.append(entry)
|
||||
|
||||
if msg.type == 'error':
|
||||
self.errors.append(msg.text)
|
||||
elif msg.type == 'warning':
|
||||
self.warnings.append(msg.text)
|
||||
|
||||
def has_critical_errors(self):
|
||||
"""Check for critical errors (not warnings)"""
|
||||
critical_patterns = [
|
||||
r"Cannot read properties of undefined",
|
||||
r"Unknown component:",
|
||||
r"Failed to hydrate",
|
||||
r"Uncaught.*Error",
|
||||
r"fetch failed",
|
||||
r"CORS",
|
||||
]
|
||||
for error in self.errors:
|
||||
for pattern in critical_patterns:
|
||||
if re.search(pattern, error, re.IGNORECASE):
|
||||
return True, error
|
||||
return False, None
|
||||
|
||||
def get_summary(self):
|
||||
"""Get log summary statistics"""
|
||||
return {
|
||||
'total_logs': len(self.logs),
|
||||
'errors': len(self.errors),
|
||||
'warnings': len(self.warnings),
|
||||
'critical_errors': self.has_critical_errors()[0]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_context():
|
||||
"""Start Playwright browser session"""
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
context = browser.new_context()
|
||||
yield context
|
||||
context.close()
|
||||
browser.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(browser_context):
|
||||
"""Create new page for each test"""
|
||||
page = browser_context.new_page()
|
||||
yield page
|
||||
page.close()
|
||||
|
||||
|
||||
def test_admin_page_loads(page):
|
||||
"""Test 1: Verify admin page loads without crashing"""
|
||||
page.goto(f"{BASE_URL}/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Check for shell component presence
|
||||
shell = page.query_selector("ds-shell")
|
||||
assert shell is not None, "Main shell component not found on page"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("component_tag,category", [
|
||||
(tag, cat) for tag, cat in COMPONENTS.items()
|
||||
])
|
||||
def test_component_loads(page, component_tag, category):
|
||||
"""
|
||||
Test 2: Verify each component loads successfully
|
||||
|
||||
Runs for all 51 components:
|
||||
- Navigate to page
|
||||
- Trigger component load
|
||||
- Check for errors
|
||||
- Verify DOM rendering
|
||||
"""
|
||||
|
||||
console_capture = ConsoleLogCapture()
|
||||
page.on("console", console_capture.handle_console_message)
|
||||
|
||||
# Navigate to admin page
|
||||
page.goto(f"{BASE_URL}/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Attempt to load component via registry
|
||||
try:
|
||||
# Check if component is in registry
|
||||
registry_check = page.evaluate(f"""
|
||||
() => {{
|
||||
const registry = window.__COMPONENT_REGISTRY__;
|
||||
return registry && '{component_tag}' in registry;
|
||||
}}
|
||||
""")
|
||||
|
||||
if not registry_check:
|
||||
pytest.skip(f"Component {component_tag} not in registry")
|
||||
|
||||
# Hydrate component
|
||||
result = page.evaluate(f"""
|
||||
async () => {{
|
||||
try {{
|
||||
const {{ hydrateComponent }} = await import('../js/config/component-registry.js');
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const element = await hydrateComponent('{component_tag}', container);
|
||||
return {{
|
||||
success: true,
|
||||
tagName: element?.tagName?.toLowerCase(),
|
||||
html: element?.outerHTML?.substring(0, 100)
|
||||
}};
|
||||
}} catch (err) {{
|
||||
return {{
|
||||
success: false,
|
||||
error: err.message
|
||||
}};
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
# Wait for component to render
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# Check results
|
||||
has_errors, error_msg = console_capture.has_critical_errors()
|
||||
|
||||
assert result['success'], f"Failed to load {component_tag}: {result.get('error')}"
|
||||
assert not has_errors, f"Critical error in {component_tag}: {error_msg}"
|
||||
|
||||
# Verify DOM element exists
|
||||
element = page.query_selector(component_tag)
|
||||
assert element is not None, f"Component {component_tag} not found in DOM after load"
|
||||
|
||||
except Exception as e:
|
||||
pytest.fail(f"Test failed for {component_tag}: {str(e)}")
|
||||
|
||||
finally:
|
||||
# Log summary
|
||||
summary = console_capture.get_summary()
|
||||
print(f"\n[{category}] {component_tag}")
|
||||
print(f" Logs: {summary['total_logs']}, Errors: {summary['errors']}, Warnings: {summary['warnings']}")
|
||||
|
||||
|
||||
def test_component_dom_rendering(page, component_tag='ds-shell', category='Layout'):
|
||||
"""
|
||||
Test 3: Verify component DOM rendering
|
||||
|
||||
Checks that component properly renders with visible content
|
||||
"""
|
||||
console_capture = ConsoleLogCapture()
|
||||
page.on("console", console_capture.handle_console_message)
|
||||
|
||||
page.goto(f"{BASE_URL}/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Check that component is in DOM
|
||||
element = page.query_selector(component_tag)
|
||||
assert element is not None, f"{component_tag} not found in initial DOM"
|
||||
|
||||
# Check that element has content (not completely blank)
|
||||
content = page.evaluate(f"""
|
||||
() => {{
|
||||
const el = document.querySelector('{component_tag}');
|
||||
return {{
|
||||
innerHTML: el?.innerHTML?.length > 0,
|
||||
textContent: el?.textContent?.trim().length > 0,
|
||||
childNodes: el?.childNodes?.length || 0,
|
||||
hasAttributes: el?.attributes?.length > 0
|
||||
}};
|
||||
}}
|
||||
""")
|
||||
|
||||
assert content['innerHTML'] or content['childNodes'] > 0, f"{component_tag} has no rendered content"
|
||||
|
||||
|
||||
def test_no_uncaught_errors(page):
|
||||
"""
|
||||
Test 4: Verify no uncaught errors during page lifecycle
|
||||
|
||||
Records all console errors and validates clean execution
|
||||
"""
|
||||
|
||||
captured_errors = []
|
||||
|
||||
def handle_error(msg):
|
||||
if msg.type == 'error':
|
||||
captured_errors.append(msg.text)
|
||||
|
||||
page.on("console", handle_error)
|
||||
page.on("pageerror", lambda err: captured_errors.append(f"Uncaught: {str(err)}"))
|
||||
|
||||
page.goto(f"{BASE_URL}/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Allow brief time for deferred errors
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# Filter out non-critical messages
|
||||
critical_errors = [
|
||||
e for e in captured_errors
|
||||
if not any(x in e for x in ['Deprecated', 'Warning', 'chrome-extension'])
|
||||
]
|
||||
|
||||
assert len(critical_errors) == 0, f"Found {len(critical_errors)} critical errors: {critical_errors[:5]}"
|
||||
|
||||
|
||||
def test_api_endpoints_available(page):
|
||||
"""
|
||||
Test 5: Verify API endpoints are available
|
||||
|
||||
Tests connectivity to backend API
|
||||
"""
|
||||
|
||||
page.goto(f"{BASE_URL}/")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Test key API endpoints
|
||||
endpoints = [
|
||||
"/api/projects",
|
||||
"/api/auth/me",
|
||||
"/api/mcp/tools",
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
try:
|
||||
response = page.evaluate(f"""
|
||||
async () => {{
|
||||
try {{
|
||||
const resp = await fetch('{endpoint}');
|
||||
return {{
|
||||
status: resp.status,
|
||||
ok: resp.ok
|
||||
}};
|
||||
}} catch (err) {{
|
||||
return {{ error: err.message }};
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
# We expect either success or a specific error (not network failure)
|
||||
assert 'error' not in response or 'fetch' not in response.get('error', ''), \
|
||||
f"Network error accessing {endpoint}: {response.get('error')}"
|
||||
|
||||
except Exception as e:
|
||||
pytest.skip(f"Could not test {endpoint}: {str(e)}")
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Test Execution & Reporting
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def print_test_header():
|
||||
"""Print header with test information"""
|
||||
print("\n" + "="*80)
|
||||
print("DSS Admin UI - Phase 1: Smoke Test (Component Loading)")
|
||||
print("="*80)
|
||||
print(f"Framework: Pytest-Playwright")
|
||||
print(f"Components: {len(COMPONENTS)} registered components")
|
||||
print(f"Base URL: {BASE_URL}")
|
||||
print(f"Timeout: {TIMEOUT}ms per component")
|
||||
print("="*80)
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""Hook to capture test results"""
|
||||
if call.when == "call":
|
||||
if "component_tag" in item.callspec.params:
|
||||
component = item.callspec.params['component_tag']
|
||||
category = item.callspec.params.get('category', 'Unknown')
|
||||
status = "✅ PASS" if item.outcomes == [] else "❌ FAIL"
|
||||
print(f" {status} [{category}] {component}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"""
|
||||
╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ DSS Admin UI - Phase 1 Smoke Test ║
|
||||
║ pytest-playwright Component Loading Validation ║
|
||||
║ ║
|
||||
║ To run tests: ║
|
||||
║ $ pytest .dss/test_smoke_phase1.py -v ║
|
||||
║ ║
|
||||
║ To run specific component: ║
|
||||
║ $ pytest .dss/test_smoke_phase1.py -k ds-shell -v ║
|
||||
║ ║
|
||||
║ To run with coverage: ║
|
||||
║ $ pytest .dss/test_smoke_phase1.py --cov=admin-ui -v ║
|
||||
║ ║
|
||||
║ To run with headless browser (CI/CD): ║
|
||||
║ $ pytest .dss/test_smoke_phase1.py --headless -v ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
Reference in New Issue
Block a user