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">� 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()
|