#!/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": "" }}, "layout_structure": {{ "score": <1-10>, "feedback": "" }}, "usability_for_craftsmen": {{ "score": <1-10>, "feedback": "" }}, "efficiency_speed": {{ "score": <1-10>, "feedback": "" }}, "issues": [ "", "" ], "overall_score": , "summary": "", "recommendations": [ "", "" ] }} """ 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""" AI Visual Test Rapport - {self.timestamp}

🤖 AI Visual Test Rapport

Automatisk UI/UX kvalitetsvurdering

Genereret: {datetime.now().strftime('%d-%m-%Y %H:%M:%S')}

Antal Tests
{total_tests}
Gennemsnitsscore
{avg_score:.1f}/10
Screenshots
{total_tests}
Status
{"✅" if avg_score >= 7 else "⚠️" if avg_score >= 5 else "❌"}
""" # 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"""

🎨 {test}

{overall:.1f}/10

📸 Screenshot

{test} screenshot
🎨 Visuelt Design {analysis['visual_design']['score']}/10
📐 Layout & Struktur {analysis['layout_structure']['score']}/10
� Brugervenlighed for Tømrere {analysis['usability_for_craftsmen']['score']}/10
⚡ Effektivitet & Hastighed {analysis['efficiency_speed']['score']}/10

Sammenfatning: {analysis.get('summary', 'Ingen sammenfatning tilgængelig')}

""" # Add issues if present if analysis.get('issues') and len(analysis['issues']) > 0: html += f"""

⚠️ Identificerede Problemer

    {''.join(f'
  • {issue}
  • ' for issue in analysis['issues'])}
""" # Add recommendations if present if analysis.get('recommendations') and len(analysis['recommendations']) > 0: html += f"""

💡 Anbefalinger

    {''.join(f'
  • {rec}
  • ' for rec in analysis['recommendations'])}
""" # Add Claude AI Agent prompt for automated fixes claude_prompt = self.generate_claude_prompt(test, analysis, screenshot_url) html += f"""

🤖 Claude AI Agent Prompt

Brug dette prompt til at få Claude AI til automatisk at fikse problemerne:

{claude_prompt}
""" html += """
""" 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()