- 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
989 lines
35 KiB
Python
989 lines
35 KiB
Python
#!/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()
|