Files
tilbudgivern/selenium_visual_ai_test.py
alexpolo1 8773d96e94 feat: Add AI-Powered Visual Testing Suite using Selenium and OpenAI Vision API
- Implemented VisualAITester class for automated UI/UX testing
- Configured environment variables for API keys and paths
- Developed methods for browser setup, login, screenshot capture, and image encoding
- Integrated OpenAI Vision API for visual quality analysis with detailed feedback
- Created tests for various application pages including homepage, project flow, geometry form, and more
- Generated comprehensive HTML report summarizing test results and AI analysis
- Added functionality to copy AI prompts for automated issue resolution
2025-11-14 20:27:19 +00:00

989 lines
35 KiB
Python
Raw Blame History

#!/usr/bin/env python3
"""
AI-Powered Visual Testing Suite
Bruger Selenium + OpenAI Vision API til at evaluere UI/UX kvalitet
"""
import time
import sys
import os
import base64
from datetime import datetime
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import TimeoutException
import openai
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Configuration
BASE_URL = "http://localhost:4031"
LOGIN_USERNAME = "toemrer"
LOGIN_PASSWORD = "REDACTED_AUTH"
SCREENSHOTS_DIR = Path("/mnt/HC_Volume_103713257/tilbudgivern/test-results/visual-ai")
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
# Create screenshots directory
SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
class VisualAITester:
"""AI-powered visual testing suite"""
def __init__(self):
self.driver = None
self.client = openai.OpenAI(api_key=OPENAI_API_KEY)
self.test_results = []
self.timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
def setup_driver(self):
"""Setup Chrome WebDriver"""
print("🔧 Starter Chrome for visuelt AI test...")
options = Options()
options.add_argument('--headless=new')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--window-size=1920,1080')
options.binary_location = '/usr/bin/google-chrome'
self.driver = webdriver.Chrome(options=options)
self.driver.implicitly_wait(5)
print("✅ Chrome klar\n")
def login(self):
"""Login to application"""
print("🔐 Logger ind...")
self.driver.get(BASE_URL)
time.sleep(2)
self.driver.find_element(By.ID, "username").send_keys(LOGIN_USERNAME)
self.driver.find_element(By.ID, "password").send_keys(LOGIN_PASSWORD)
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
print("✅ Login gennemført\n")
def take_screenshot(self, name: str, description: str) -> str:
"""Take screenshot and return path"""
filename = f"{self.timestamp}_{name}.png"
filepath = SCREENSHOTS_DIR / filename
# Scroll to top first
self.driver.execute_script("window.scrollTo(0, 0);")
time.sleep(1)
self.driver.save_screenshot(str(filepath))
print(f"📸 Screenshot: {filename}")
return str(filepath)
def encode_image(self, image_path: str) -> str:
"""Encode image to base64"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def analyze_visual_quality(self, image_path: str, context: str) -> dict:
"""Use OpenAI Vision API to analyze UI/UX quality"""
print(f"🤖 AI analyserer: {context}...")
base64_image = self.encode_image(image_path)
prompt = f"""
Analyser dette screenshot af en dansk tømrer-software applikation ({context}).
**KRITISK KONTEKST - JANNICKS FEEDBACK-BASERET EVALUERING:**
Denne software er blevet udviklet baseret på specifik feedback fra en professionel tømrer (Jannick).
Hans tidligere ønsker og designfilosofi skal guide din evaluering:
**JANNICKS FEEDBACK PRIORITETER:**
1. **SIMPLICITET OVER KOMPLEKSITET:**
❌ Fjern "kompleks tag" og andre komplicerede kategorier
❌ Ingen tekniske detaljer som almindelige tømrere ikke bruger
✅ Simple input-felter (f.eks. kvist: kun størrelse + antal)
✅ Clean labels uden parentes eller kompliceret tekst
2. **AUTOMATISERING & HASTIGHED:**
✅ Auto-gem funktioner (ingen unødvendige "gem" knapper)
✅ Scroll-to-top ved tab-skift for bedre navigation
✅ Hurtig navigation mellem sektioner
❌ Ingen unødvendige bekræftelses-dialoger
3. **REDIGERBARHED NÅR DET BETYDER NOGET:**
✅ Timer og priser skal kunne redigeres i Final Review
✅ Tilbudstekst skal være redigerbar før afsendelse
✅ Fortjeneste procent synlig i final review
4. **VISUEL KLARHED:**
✅ Hvid baggrund på materialer (IKKE gul)
✅ Professionelt udtryk til at vise kunder
✅ Store, læsbare skrifttyper
❌ Ingen rod eller overflødig information
5. **PRAKTISKE FUNKTIONER:**
✅ PDF knapper skal fungere korrekt
✅ Adresse skal pulles automatisk fra kunde-data
✅ AI generator skriver i firmaets professionelle tone
**DIN EVALUERINGSOPGAVE:**
Vurder om dette screenshot følger Jannicks designfilosofi:
- Er det ENKELT nok for en tømrer uden IT-erfaring?
- Er det HURTIGT - minimal klik, maksimal effektivitet?
- Ser det PROFESSIONELT ud til at vise til kunder?
- Kan kritiske data (priser/timer) REDIGERES hvor nødvendigt?
- Er der unødvendig kompleksitet der skal fjernes?
Evaluer følgende på en skala fra 1-10 med JANNICKS PERSPEKTIV:
1. **Visuelt Design** (1-10)
- Professionelt nok til at vise kunder?
- Clean og simpelt uden unødvendigt pynt?
- Behagelige farver (hvid baggrund på materialer)?
2. **Layout & Struktur** (1-10)
- Logisk flow for tømrer-workflow?
- Vigtigste funktioner synlige uden scroll?
- Ingen forvirrende kategorier eller labels?
3. **Brugervenlighed for Tømrere** (1-10)
- Kan bruges uden IT-erfaring?
- Store nok knapper/felter til touch?
- Korrekte fagtermer uden teknisk jargon?
4. **Effektivitet & Hastighed** (1-10)
- Minimal klik til at nå målet?
- Auto-gem hvor det giver mening?
- Hurtig navigation mellem sektioner?
Giv dit svar i følgende JSON format:
{{
"visual_design": {{
"score": <1-10>,
"feedback": "<specifik feedback baseret på Jannicks filosofi på dansk>"
}},
"layout_structure": {{
"score": <1-10>,
"feedback": "<specifik feedback baseret på Jannicks filosofi på dansk>"
}},
"usability_for_craftsmen": {{
"score": <1-10>,
"feedback": "<specifik feedback baseret på Jannicks filosofi på dansk>"
}},
"efficiency_speed": {{
"score": <1-10>,
"feedback": "<specifik feedback baseret på Jannicks filosofi på dansk>"
}},
"issues": [
"<konkret problem som ville frustrere Jannick eller andre tømrere>",
"<konkret problem som ville frustrere Jannick eller andre tømrere>"
],
"overall_score": <gennemsnit>,
"summary": "<kort sammenfatning: følger dette Jannicks designfilosofi?>",
"recommendations": [
"<praktisk anbefaling baseret på Jannicks tidligere feedback>",
"<praktisk anbefaling baseret på Jannicks tidligere feedback>"
]
}}
"""
try:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{base64_image}",
"detail": "high"
}
}
]
}
],
max_tokens=2000,
temperature=0.3
)
analysis = response.choices[0].message.content
# Parse JSON from response
import json
# Extract JSON from markdown code blocks if present
if "```json" in analysis:
analysis = analysis.split("```json")[1].split("```")[0].strip()
elif "```" in analysis:
analysis = analysis.split("```")[1].split("```")[0].strip()
result = json.loads(analysis)
# Normalize keys if AI used old names
if 'usability' in result and 'usability_for_craftsmen' not in result:
result['usability_for_craftsmen'] = result.pop('usability')
if 'professional_appearance' in result and 'efficiency_speed' not in result:
result['efficiency_speed'] = result.pop('professional_appearance')
print(f"✅ AI Analyse gennemført - Overall Score: {result.get('overall_score', 'N/A')}/10\n")
return result
except Exception as e:
print(f"❌ AI analyse fejlede: {e}\n")
return {
"error": str(e),
"visual_design": {"score": 0, "feedback": "Analyse fejlede"},
"layout_structure": {"score": 0, "feedback": "Analyse fejlede"},
"usability_for_craftsmen": {"score": 0, "feedback": "Analyse fejlede"},
"efficiency_speed": {"score": 0, "feedback": "Analyse fejlede"},
"overall_score": 0,
"summary": f"AI analyse fejlede: {e}"
}
def test_homepage(self):
"""Test homepage visual quality"""
print("=" * 80)
print("🏠 TEST: Homepage / Dashboard")
print("=" * 80)
self.driver.get(f"{BASE_URL}/dashboard")
time.sleep(3)
screenshot = self.take_screenshot("homepage", "Main dashboard view")
analysis = self.analyze_visual_quality(screenshot, "Hovedside/Dashboard")
self.test_results.append({
"test": "Homepage",
"screenshot": screenshot,
"analysis": analysis
})
def test_projekt_flow(self):
"""Test Projekt Flow page"""
print("=" * 80)
print("🏗️ TEST: Projekt Flow")
print("=" * 80)
# Navigate to Projekt Flow
nav_buttons = self.driver.find_elements(By.CLASS_NAME, "nav-btn")
for btn in nav_buttons:
if "Projekt Flow" in btn.text:
btn.click()
break
time.sleep(3)
screenshot = self.take_screenshot("projekt_flow", "Project flow interface")
analysis = self.analyze_visual_quality(screenshot, "Projekt Flow - Step interface")
self.test_results.append({
"test": "Projekt Flow",
"screenshot": screenshot,
"analysis": analysis
})
def test_geometry_form(self):
"""Test Geometry input form"""
print("=" * 80)
print("📐 TEST: Geometri Form")
print("=" * 80)
# Create a test project first
try:
project_name = self.driver.find_element(By.ID, "projectName")
project_name.clear()
project_name.send_keys("AI Visual Test Project")
customer_name = self.driver.find_element(By.ID, "customerName")
customer_name.clear()
customer_name.send_keys("Test Kunde")
save_buttons = self.driver.find_elements(By.XPATH,
"//button[contains(text(), 'Næste') or contains(text(), 'Gem')]")
if save_buttons:
save_buttons[0].click()
time.sleep(3)
except:
pass
# Click on Geometry step
try:
steps = self.driver.find_elements(By.CLASS_NAME, "step")
if len(steps) >= 2:
steps[1].click()
time.sleep(3)
except:
pass
# Scroll to show form
self.driver.execute_script("window.scrollTo(0, 400);")
time.sleep(1)
screenshot = self.take_screenshot("geometry_form", "Geometry input form with fields")
analysis = self.analyze_visual_quality(screenshot, "Geometri Form - Input felter og visualisering")
self.test_results.append({
"test": "Geometri Form",
"screenshot": screenshot,
"analysis": analysis
})
def test_smart_packages(self):
"""Test Smart Packages interface"""
print("=" * 80)
print("📦 TEST: Smart Pakker")
print("=" * 80)
# Navigate to Smart Pakker
nav_buttons = self.driver.find_elements(By.CLASS_NAME, "nav-btn")
for btn in nav_buttons:
if "Smart Pakker" in btn.text:
btn.click()
break
time.sleep(3)
screenshot = self.take_screenshot("smart_packages", "Smart packages selection interface")
analysis = self.analyze_visual_quality(screenshot, "Smart Pakker - Pakke oversigt")
self.test_results.append({
"test": "Smart Pakker",
"screenshot": screenshot,
"analysis": analysis
})
def test_materials_manager(self):
"""Test Materials Manager interface"""
print("=" * 80)
print("🔧 TEST: Materialestyring")
print("=" * 80)
# Navigate to Materialer
nav_buttons = self.driver.find_elements(By.CLASS_NAME, "nav-btn")
for btn in nav_buttons:
if "Materialer" in btn.text:
btn.click()
break
time.sleep(3)
screenshot = self.take_screenshot("materials_manager", "Materials management interface")
analysis = self.analyze_visual_quality(screenshot, "Materialestyring - Oversigt og håndtering")
self.test_results.append({
"test": "Materialestyring",
"screenshot": screenshot,
"analysis": analysis
})
def test_planning_dashboard(self):
"""Test Planning Dashboard"""
print("=" * 80)
print("📋 TEST: Planlægning Dashboard")
print("=" * 80)
# Navigate to Planlægning
nav_buttons = self.driver.find_elements(By.CLASS_NAME, "nav-btn")
for btn in nav_buttons:
if "Planlægning" in btn.text:
btn.click()
break
time.sleep(3)
screenshot = self.take_screenshot("planning_dashboard", "Planning dashboard with orders and calendar")
analysis = self.analyze_visual_quality(screenshot, "Planlægning Dashboard - Ordrestyring og kalender")
self.test_results.append({
"test": "Planlægning Dashboard",
"screenshot": screenshot,
"analysis": analysis
})
def test_noegletal(self):
"""Test Nøgletal/Analytics Dashboard"""
print("=" * 80)
print("📊 TEST: Nøgletal & Analyser")
print("=" * 80)
# Navigate to Nøgletal
nav_buttons = self.driver.find_elements(By.CLASS_NAME, "nav-btn")
for btn in nav_buttons:
if "Nøgletal" in btn.text or "📊" in btn.text:
btn.click()
break
time.sleep(3)
screenshot = self.take_screenshot("noegletal", "Analytics and key metrics dashboard")
analysis = self.analyze_visual_quality(screenshot, "Nøgletal Dashboard - KPI'er og forretningsanalyse")
self.test_results.append({
"test": "Nøgletal & Analyser",
"screenshot": screenshot,
"analysis": analysis
})
def test_openai_usage(self):
"""Test OpenAI Usage Statistics"""
print("=" * 80)
print("🤖 TEST: OpenAI Forbrug")
print("=" * 80)
# Navigate to OpenAI Usage
nav_buttons = self.driver.find_elements(By.CLASS_NAME, "nav-btn")
for btn in nav_buttons:
if "AI Forbrug" in btn.text or "OpenAI" in btn.text:
btn.click()
break
time.sleep(3)
screenshot = self.take_screenshot("openai_usage", "OpenAI usage and cost statistics")
analysis = self.analyze_visual_quality(screenshot, "OpenAI Forbrug - Statistikker over API brug")
self.test_results.append({
"test": "OpenAI Forbrug",
"screenshot": screenshot,
"analysis": analysis
})
def generate_claude_prompt(self, test_name, analysis, screenshot_url):
"""Generate a Claude AI agent prompt for fixing identified issues"""
issues = analysis.get('issues', [])
recommendations = analysis.get('recommendations', [])
overall_score = analysis.get('overall_score', 0)
summary = analysis.get('summary', '')
# Map test names to file paths
file_mapping = {
"Homepage / Dashboard": "frontend/src/App.js",
"Projekt Flow": "frontend/src/components/ProjectFlow.js",
"Geometri Form": "frontend/src/components/EnhancedGeometry.js",
"Smart Pakker": "frontend/src/components/SmartPackagesEnhanced.js",
"Materialestyring": "frontend/src/components/MaterialsManager.js",
"Planlægning Dashboard": "frontend/src/components/PlanningDashboard.js",
"Nøgletal & Analyser": "frontend/src/components/AnalyticsDashboard.js",
"OpenAI Forbrug": "frontend/src/components/OpenAIUsage.js"
}
relevant_files = file_mapping.get(test_name, "frontend/src/components/")
prompt = {
"task": f"Fix UI/UX issues in {test_name}",
"context": {
"component": test_name,
"current_score": f"{overall_score}/10",
"user_perspective": "Tømrer (carpenter) - needs fast, simple, efficient workflow",
"evaluation_summary": summary
},
"identified_issues": issues,
"recommendations": recommendations,
"files_to_modify": [relevant_files],
"evaluation_criteria": {
"visual_design": analysis.get('visual_design', {}),
"layout_structure": analysis.get('layout_structure', {}),
"usability_for_craftsmen": analysis.get('usability_for_craftsmen', {}),
"efficiency_speed": analysis.get('efficiency_speed', {})
},
"instructions": [
f"Analyze the component at: {relevant_files}",
"Address all identified issues from the AI visual analysis",
"Implement the recommendations provided",
"Focus on improving usability for craftsmen (tømrere) - minimal clicks, fast workflow",
"Ensure professional appearance suitable for showing to customers",
"Maintain existing functionality while improving UX",
"Test changes with npm run build and verify visually"
],
"success_criteria": [
"All identified issues are resolved",
"Overall score improves to 8+/10",
"Carpenter workflow becomes faster and more intuitive",
"Professional appearance maintained or improved"
],
"reference_screenshot": screenshot_url
}
import json
return json.dumps(prompt, indent=2, ensure_ascii=False)
def generate_report(self):
"""Generate comprehensive HTML report"""
print("\n" + "=" * 80)
print("📊 GENERERER RAPPORT")
print("=" * 80)
report_path = SCREENSHOTS_DIR / f"visual_ai_report_{self.timestamp}.html"
# Calculate overall statistics
total_tests = len(self.test_results)
avg_score = sum(r['analysis'].get('overall_score', 0) for r in self.test_results) / total_tests if total_tests > 0 else 0
# Generate HTTP URLs for screenshots
base_url = "http://localhost:4031"
# Generate HTML
html = f"""
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Visual Test Rapport - {self.timestamp}</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
color: #333;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}}
.header h1 {{
font-size: 2.5rem;
margin-bottom: 10px;
}}
.header p {{
font-size: 1.2rem;
opacity: 0.9;
}}
.summary {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 30px;
background: #f8f9fa;
}}
.stat-card {{
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
text-align: center;
}}
.stat-card .value {{
font-size: 2.5rem;
font-weight: bold;
color: #667eea;
margin: 10px 0;
}}
.stat-card .label {{
color: #666;
font-size: 0.9rem;
}}
.test-result {{
padding: 40px;
border-bottom: 1px solid #e0e0e0;
}}
.test-result:last-child {{
border-bottom: none;
}}
.test-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}}
.test-title {{
font-size: 1.8rem;
color: #333;
}}
.overall-score {{
font-size: 2rem;
font-weight: bold;
padding: 10px 20px;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}}
.test-content {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 20px;
}}
.screenshot-section {{
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
}}
.screenshot-section img {{
width: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border: 1px solid #ddd;
}}
.analysis-section {{
display: flex;
flex-direction: column;
gap: 20px;
}}
.metric {{
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #667eea;
}}
.metric-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}}
.metric-name {{
font-weight: 600;
color: #333;
}}
.metric-score {{
font-size: 1.2rem;
font-weight: bold;
color: #667eea;
}}
.metric-feedback {{
color: #666;
font-size: 0.95rem;
line-height: 1.6;
}}
.issues {{
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}}
.issues h3 {{
color: #856404;
margin-bottom: 10px;
}}
.issues ul {{
list-style: none;
padding: 0;
}}
.issues li {{
padding: 8px 0;
border-bottom: 1px solid #ffe69c;
color: #856404;
}}
.issues li:last-child {{
border-bottom: none;
}}
.recommendations {{
background: #d4edda;
border: 1px solid #28a745;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}}
.recommendations h3 {{
color: #155724;
margin-bottom: 10px;
}}
.recommendations ul {{
list-style: none;
padding: 0;
}}
.recommendations li {{
padding: 8px 0;
color: #155724;
border-bottom: 1px solid #c3e6cb;
}}
.recommendations li:last-child {{
border-bottom: none;
}}
.recommendations li:before {{
content: "";
color: #28a745;
font-weight: bold;
}}
.summary-text {{
background: #e7f3ff;
border-left: 4px solid #2196F3;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
}}
.summary-text p {{
color: #0d47a1;
line-height: 1.6;
}}
.claude-prompt-section {{
background: #f0f4ff;
border: 2px solid #667eea;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}}
.claude-prompt-section h3 {{
color: #667eea;
margin-bottom: 10px;
}}
.claude-prompt-section pre {{
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
border: 1px solid #ddd;
font-size: 0.85rem;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
}}
.claude-prompt-section button {{
background: #667eea;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
margin-bottom: 10px;
transition: background 0.3s;
}}
.claude-prompt-section button:hover {{
background: #5568d3;
}}
.score-excellent {{ color: #28a745; }}
.score-good {{ color: #5cb85c; }}
.score-okay {{ color: #ffc107; }}
.score-poor {{ color: #dc3545; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 AI Visual Test Rapport</h1>
<p>Automatisk UI/UX kvalitetsvurdering</p>
<p style="font-size: 0.9rem; margin-top: 10px;">Genereret: {datetime.now().strftime('%d-%m-%Y %H:%M:%S')}</p>
</div>
<div class="summary">
<div class="stat-card">
<div class="label">Antal Tests</div>
<div class="value">{total_tests}</div>
</div>
<div class="stat-card">
<div class="label">Gennemsnitsscore</div>
<div class="value">{avg_score:.1f}/10</div>
</div>
<div class="stat-card">
<div class="label">Screenshots</div>
<div class="value">{total_tests}</div>
</div>
<div class="stat-card">
<div class="label">Status</div>
<div class="value" style="font-size: 2rem;">{"" if avg_score >= 7 else "⚠️" if avg_score >= 5 else ""}</div>
</div>
</div>
"""
# Add each test result
for result in self.test_results:
test = result['test']
analysis = result['analysis']
screenshot = result['screenshot']
# Get just the filename for HTTP URL
screenshot_filename = Path(screenshot).name
screenshot_url = f"{base_url}/api/visual-reports/{screenshot_filename}"
# Determine score color class
overall = analysis.get('overall_score', 0)
score_class = 'score-excellent' if overall >= 8 else 'score-good' if overall >= 6 else 'score-okay' if overall >= 4 else 'score-poor'
html += f"""
<div class="test-result">
<div class="test-header">
<h2 class="test-title">🎨 {test}</h2>
<div class="overall-score {score_class}">{overall:.1f}/10</div>
</div>
<div class="test-content">
<div class="screenshot-section">
<h3 style="margin-bottom: 15px;">📸 Screenshot</h3>
<img src="{screenshot_url}" alt="{test} screenshot">
</div>
<div class="analysis-section">
<div class="metric">
<div class="metric-header">
<span class="metric-name">🎨 Visuelt Design</span>
<span class="metric-score">{analysis['visual_design']['score']}/10</span>
</div>
<div class="metric-feedback">{analysis['visual_design']['feedback']}</div>
</div>
<div class="metric">
<div class="metric-header">
<span class="metric-name">📐 Layout & Struktur</span>
<span class="metric-score">{analysis['layout_structure']['score']}/10</span>
</div>
<div class="metric-feedback">{analysis['layout_structure']['feedback']}</div>
</div>
<div class="metric">
<div class="metric-header">
<span class="metric-name"><3E> Brugervenlighed for Tømrere</span>
<span class="metric-score">{analysis['usability_for_craftsmen']['score']}/10</span>
</div>
<div class="metric-feedback">{analysis['usability_for_craftsmen']['feedback']}</div>
</div>
<div class="metric">
<div class="metric-header">
<span class="metric-name">⚡ Effektivitet & Hastighed</span>
<span class="metric-score">{analysis['efficiency_speed']['score']}/10</span>
</div>
<div class="metric-feedback">{analysis['efficiency_speed']['feedback']}</div>
</div>
</div>
</div>
<div class="summary-text">
<p><strong>Sammenfatning:</strong> {analysis.get('summary', 'Ingen sammenfatning tilgængelig')}</p>
</div>
"""
# Add issues if present
if analysis.get('issues') and len(analysis['issues']) > 0:
html += f"""
<div class="issues">
<h3>⚠️ Identificerede Problemer</h3>
<ul>
{''.join(f'<li>{issue}</li>' for issue in analysis['issues'])}
</ul>
</div>
"""
# Add recommendations if present
if analysis.get('recommendations') and len(analysis['recommendations']) > 0:
html += f"""
<div class="recommendations">
<h3>💡 Anbefalinger</h3>
<ul>
{''.join(f'<li>{rec}</li>' for rec in analysis['recommendations'])}
</ul>
</div>
"""
# Add Claude AI Agent prompt for automated fixes
claude_prompt = self.generate_claude_prompt(test, analysis, screenshot_url)
html += f"""
<div class="claude-prompt-section">
<h3>🤖 Claude AI Agent Prompt</h3>
<p style="color: #666; margin-bottom: 10px; font-size: 0.9rem;">
Brug dette prompt til at få Claude AI til automatisk at fikse problemerne:
</p>
<button onclick="copyClaudePrompt_{test.replace(' ', '_').replace('&', '').replace('/', '').lower()}"
style="background: #667eea; color: white; border: none; padding: 8px 16px; border-radius: 5px; cursor: pointer; margin-bottom: 10px;">
📋 Kopiér Prompt
</button>
<pre id="claude_prompt_{test.replace(' ', '_').replace('&', '').replace('/', '').lower()}" style="background: #f8f9fa; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #ddd; font-size: 0.85rem; line-height: 1.5;">{claude_prompt}</pre>
<script>
function copyClaudePrompt_{test.replace(' ', '_').replace('&', '').replace('/', '').lower()}() {{
const promptText = document.getElementById('claude_prompt_{test.replace(' ', '_').replace('&', '').replace('/', '').lower()}').textContent;
navigator.clipboard.writeText(promptText).then(() => {{
alert('✅ Claude prompt kopieret til clipboard!');
}});
}}
</script>
</div>
"""
html += """
</div>
"""
html += """
</div>
</body>
</html>
"""
# Save report
with open(report_path, 'w', encoding='utf-8') as f:
f.write(html)
# Generate HTTP URL for report
report_filename = Path(report_path).name
report_url = f"{base_url}/api/visual-reports/{report_filename}"
latest_url = f"{base_url}/api/visual-reports/latest"
print(f"\n✅ Rapport gemt: {report_path}")
print(f"🌐 HTTP URL: {report_url}")
print(f"🌐 Latest URL: {latest_url}")
print(f"📊 Gennemsnitsscore: {avg_score:.1f}/10")
print(f"🎯 Status: {'Excellent ✅' if avg_score >= 8 else 'Good ✅' if avg_score >= 6 else 'Needs Improvement ⚠️' if avg_score >= 4 else 'Poor ❌'}")
return report_url
def run_all_tests(self):
"""Run all visual tests"""
try:
self.setup_driver()
self.login()
# Run tests
self.test_homepage()
self.test_projekt_flow()
self.test_geometry_form()
self.test_smart_packages()
self.test_materials_manager()
self.test_planning_dashboard()
self.test_noegletal()
self.test_openai_usage()
# Generate report
report_path = self.generate_report()
print("\n" + "=" * 80)
print("✅ ALLE TESTS GENNEMFØRT!")
print("=" * 80)
print(f"\n📄 Åbn rapporten: {report_path}")
return True
except Exception as e:
print(f"\n❌ Test suite fejlede: {e}")
import traceback
traceback.print_exc()
return False
finally:
if self.driver:
self.driver.quit()
print("\n🔒 Browser lukket")
def main():
"""Main entry point"""
print("=" * 80)
print("🤖 AI-POWERED VISUAL TESTING SUITE")
print("=" * 80)
print()
if not OPENAI_API_KEY:
print("❌ OPENAI_API_KEY ikke fundet i .env fil!")
print("Tilføj din API key til .env filen:")
print("OPENAI_API_KEY=sk-your-key-here")
return sys.exit(1)
tester = VisualAITester()
success = tester.run_all_tests()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()