diff --git a/frontend/Screenshot 2025-05-19 112503.png b/frontend/Screenshot 2025-05-19 112503.png new file mode 100644 index 0000000000..9fb242ab08 Binary files /dev/null and b/frontend/Screenshot 2025-05-19 112503.png differ diff --git a/frontend/Screenshot 2025-05-19 112535.png b/frontend/Screenshot 2025-05-19 112535.png new file mode 100644 index 0000000000..c0ec06a293 Binary files /dev/null and b/frontend/Screenshot 2025-05-19 112535.png differ diff --git a/frontend/ai/.gitignore b/frontend/ai/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/fastapi_backend.py b/frontend/fastapi_backend.py new file mode 100644 index 0000000000..6002dbc9fd --- /dev/null +++ b/frontend/fastapi_backend.py @@ -0,0 +1,981 @@ +from fastapi import FastAPI, HTTPException, File, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, HttpUrl +from typing import Optional, List, Dict, Any +import json +import asyncio +from datetime import datetime +import hashlib +import subprocess +import tempfile +import os +from motor.motor_asyncio import AsyncIOMotorClient +from pymongo import MongoClient +import logging +from playwright.async_api import async_playwright +import base64 + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = FastAPI(title="LLM UI Testing Framework", version="2.0.0") + +# Enable CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# MongoDB Configuration +MONGODB_URL = os.getenv("MONGODB_URL", "mongodb://localhost:27017") +DATABASE_NAME = "llm_testing_framework" +COLLECTION_NAME = "sessions" + +# MongoDB client +client = AsyncIOMotorClient(MONGODB_URL) +db = client[DATABASE_NAME] +sessions_collection = db[COLLECTION_NAME] + +# Pydantic models +class ElementExtractionRequest(BaseModel): + web_url: HttpUrl + config_content: str + system_prompt: str + user_prompt: str + force_new_session: bool = False + +class UrlHashCheckRequest(BaseModel): + web_url: HttpUrl + +class UrlHashCheckResponse(BaseModel): + url: str + current_hash: str + existing_sessions: List[Dict[str, Any]] + has_existing_session: bool + +class ElementExtractionResponse(BaseModel): + session_id: str + url_hash: str + extracted_elements: List[Dict[str, Any]] + timestamp: datetime + is_new_session: bool + +class TestcaseGenerationRequest(BaseModel): + session_id: str + extracted_elements: List[Dict[str, Any]] + system_prompt: str + user_prompt: str + testcase_sample: Optional[str] = None + +class TestcaseGenerationResponse(BaseModel): + session_id: str + test_cases: List[Dict[str, Any]] + timestamp: datetime + +class CodeGenerationRequest(BaseModel): + session_id: str + test_cases: List[Dict[str, Any]] + system_prompt: str + user_prompt: str + +class CodeGenerationResponse(BaseModel): + session_id: str + generated_code: str + timestamp: datetime + +class CodeExecutionRequest(BaseModel): + session_id: str + code: str + execution_type: str = "selenium" + +class CodeExecutionResponse(BaseModel): + session_id: str + execution_result: Dict[str, Any] + timestamp: datetime + +class SessionStatus(BaseModel): + session_id: str + url_hash: str + web_url: str + current_stage: str + completed_stages: List[str] + results: Dict[str, Any] + created_at: datetime + updated_at: datetime + +# Enhanced element extraction using Playwright +async def extract_elements_with_playwright(url: str, config: dict = None) -> List[Dict[str, Any]]: + """ + Comprehensively extract all testable elements from web page using Playwright + """ + elements = [] + + try: + async with async_playwright() as p: + # Launch browser with robust settings + browser = await p.chromium.launch( + headless=True, + args=['--no-sandbox', '--disable-dev-shm-usage'] + ) + + context = await browser.new_context( + viewport={'width': 1920, 'height': 1080}, + user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ) + + page = await context.new_page() + + # Set longer timeout for complex pages + page.set_default_timeout(30000) + + # Navigate to URL with error handling + try: + await page.goto(url, wait_until='networkidle', timeout=30000) + await page.wait_for_load_state('domcontentloaded') + await asyncio.sleep(2) # Allow dynamic content to load + except Exception as e: + logger.warning(f"Page load issue for {url}: {e}") + # Try alternative loading strategy + await page.goto(url, wait_until='domcontentloaded', timeout=15000) + await asyncio.sleep(3) + + # Comprehensive element selectors + selectors = { + 'buttons': [ + 'button', 'input[type="button"]', 'input[type="submit"]', + 'input[type="reset"]', '[role="button"]', '.btn', '.button', + 'a[href^="javascript"]', '[onclick]' + ], + 'inputs': [ + 'input[type="text"]', 'input[type="email"]', 'input[type="password"]', + 'input[type="number"]', 'input[type="tel"]', 'input[type="url"]', + 'input[type="search"]', 'input[type="date"]', 'input[type="time"]', + 'input[type="datetime-local"]', 'input[type="month"]', 'input[type="week"]', + 'textarea', 'input:not([type])' + ], + 'selects': ['select', '[role="combobox"]', '[role="listbox"]'], + 'checkboxes': ['input[type="checkbox"]', '[role="checkbox"]'], + 'radios': ['input[type="radio"]', '[role="radio"]'], + 'links': ['a[href]', '[role="link"]'], + 'interactive': [ + '[tabindex]', '[role="tab"]', '[role="menuitem"]', + '[role="option"]', '[role="treeitem"]', '[contenteditable="true"]' + ], + 'forms': ['form'], + 'images': ['img[alt]', 'img[title]', '[role="img"]'], + 'headings': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + 'landmarks': [ + '[role="navigation"]', '[role="main"]', '[role="banner"]', + '[role="contentinfo"]', '[role="complementary"]', '[role="search"]' + ] + } + + # Extract elements by category + for category, selector_list in selectors.items(): + for selector in selector_list: + try: + category_elements = await page.locator(selector).all() + + for element in category_elements: + try: + # Get comprehensive element data + element_data = await extract_element_data(element, category, page) + if element_data and element_data.get('is_visible', False): + elements.append(element_data) + + except Exception as e: + logger.debug(f"Error extracting element data: {e}") + continue + + except Exception as e: + logger.debug(f"Error with selector {selector}: {e}") + continue + + # Take screenshot for visual reference + screenshot = await page.screenshot(full_page=True) + screenshot_b64 = base64.b64encode(screenshot).decode() + + await browser.close() + + # Remove duplicates and sort elements + unique_elements = remove_duplicate_elements(elements) + + # Add metadata + metadata = { + 'total_elements': len(unique_elements), + 'extraction_timestamp': datetime.now().isoformat(), + 'url': url, + 'screenshot': screenshot_b64[:1000] + '...' if len(screenshot_b64) > 1000 else screenshot_b64, # Truncate for hash + 'categories': {category: len([e for e in unique_elements if e.get('category') == category]) + for category in selectors.keys()} + } + + return unique_elements + [{'type': 'metadata', 'data': metadata}] + + except Exception as e: + logger.error(f"Playwright extraction failed for {url}: {e}") + raise HTTPException(status_code=500, detail=f"Element extraction failed: {str(e)}") + +async def extract_element_data(element, category: str, page) -> Dict[str, Any]: + """Extract comprehensive data from a single element""" + try: + # Check if element is visible and interactable + is_visible = await element.is_visible() + is_enabled = await element.is_enabled() if category in ['buttons', 'inputs', 'selects'] else True + + if not is_visible: + return None + + # Get bounding box + bbox = await element.bounding_box() + + # Get element attributes + tag_name = await element.evaluate('el => el.tagName.toLowerCase()') + + # Common attributes + attrs = {} + for attr in ['id', 'class', 'name', 'type', 'value', 'placeholder', 'title', 'alt', 'href', 'src', 'role']: + try: + attr_value = await element.get_attribute(attr) + if attr_value: + attrs[attr] = attr_value + except: + pass + + # Get text content + text_content = '' + try: + text_content = (await element.text_content() or '').strip() + if not text_content and category == 'inputs': + text_content = attrs.get('placeholder', '') + except: + pass + + # Generate robust selectors + selectors = await generate_robust_selectors(element, page) + + element_data = { + 'category': category, + 'tag_name': tag_name, + 'text': text_content, + 'attributes': attrs, + 'selectors': selectors, + 'bounding_box': bbox, + 'is_visible': is_visible, + 'is_enabled': is_enabled, + 'element_type': determine_element_type(tag_name, attrs, category) + } + + return element_data + + except Exception as e: + logger.debug(f"Error extracting element data: {e}") + return None + +async def generate_robust_selectors(element, page) -> Dict[str, str]: + """Generate multiple robust selectors for an element""" + selectors = {} + + try: + # CSS selectors + selectors['css'] = await element.evaluate(''' + el => { + let selector = el.tagName.toLowerCase(); + if (el.id) selector += '#' + el.id; + if (el.className) selector += '.' + el.className.split(' ').join('.'); + return selector; + } + ''') + + # XPath + selectors['xpath'] = await element.evaluate(''' + el => { + let path = ''; + while (el && el.nodeType === Node.ELEMENT_NODE) { + let siblingIndex = 1; + let sibling = el.previousSibling; + while (sibling) { + if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === el.tagName) { + siblingIndex++; + } + sibling = sibling.previousSibling; + } + path = el.tagName.toLowerCase() + '[' + siblingIndex + ']' + (path ? '/' + path : ''); + el = el.parentNode; + } + return '//' + path; + } + ''') + + # Text-based selector if element has text + text = await element.text_content() + if text and text.strip(): + selectors['text'] = f"text='{text.strip()[:50]}'" + + # Attribute-based selectors + id_attr = await element.get_attribute('id') + if id_attr: + selectors['id'] = f"[id='{id_attr}']" + + name_attr = await element.get_attribute('name') + if name_attr: + selectors['name'] = f"[name='{name_attr}']" + + except Exception as e: + logger.debug(f"Error generating selectors: {e}") + + return selectors + +def determine_element_type(tag_name: str, attrs: Dict, category: str) -> str: + """Determine specific element type for testing purposes""" + if tag_name == 'input': + input_type = attrs.get('type', 'text') + return f"input_{input_type}" + elif tag_name == 'button': + return 'button' + elif tag_name == 'a': + return 'link' + elif tag_name == 'select': + return 'dropdown' + elif tag_name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + return 'heading' + elif tag_name == 'form': + return 'form' + elif attrs.get('role'): + return f"role_{attrs['role']}" + else: + return tag_name + +def remove_duplicate_elements(elements: List[Dict]) -> List[Dict]: + """Remove duplicate elements based on selectors and position""" + seen = set() + unique_elements = [] + + for element in elements: + # Create a unique key based on selectors and position + selectors = element.get('selectors', {}) + bbox = element.get('bounding_box', {}) + + key = f"{selectors.get('css', '')}-{selectors.get('xpath', '')}-{bbox.get('x', 0)}-{bbox.get('y', 0)}" + + if key not in seen: + seen.add(key) + unique_elements.append(element) + + return unique_elements + +def calculate_url_hash(elements: List[Dict[str, Any]], url: str) -> str: + """Calculate hash based on extracted elements and URL structure""" + # Create a stable representation for hashing + hash_data = { + 'url': url, + 'elements_count': len([e for e in elements if e.get('type') != 'metadata']), + 'structure': [] + } + + # Sort elements by position for consistent hashing + sorted_elements = sorted( + [e for e in elements if e.get('type') != 'metadata'], + key=lambda x: ( + x.get('bounding_box', {}).get('y', 0), + x.get('bounding_box', {}).get('x', 0), + x.get('tag_name', ''), + x.get('text', '') + ) + ) + + # Create structural fingerprint + for element in sorted_elements: + structure_item = { + 'tag': element.get('tag_name', ''), + 'type': element.get('element_type', ''), + 'text_hash': hashlib.md5((element.get('text', '') or '').encode()).hexdigest()[:8], + 'attrs_hash': hashlib.md5(str(sorted(element.get('attributes', {}).items())).encode()).hexdigest()[:8] + } + hash_data['structure'].append(structure_item) + + # Generate hash + hash_string = json.dumps(hash_data, sort_keys=True) + return hashlib.sha256(hash_string.encode()).hexdigest()[:16] + +# Database operations with hash-based sessions +async def check_existing_sessions(url: str, current_hash: str) -> Dict[str, Any]: + """Check for existing sessions for the given URL""" + try: + # Find all sessions for this URL + cursor = sessions_collection.find( + {"web_url": url} + ).sort("updated_at", -1) + + existing_sessions = await cursor.to_list(length=50) + + # Check if current hash matches any existing session + matching_session = None + for session in existing_sessions: + if session.get('url_hash') == current_hash: + matching_session = session + break + + return { + 'url': url, + 'current_hash': current_hash, + 'existing_sessions': [ + { + 'session_id': session['session_id'], + 'url_hash': session['url_hash'], + 'current_stage': session['current_stage'], + 'completed_stages': session['completed_stages'], + 'created_at': session['created_at'], + 'updated_at': session['updated_at'] + } + for session in existing_sessions[:10] # Limit to recent 10 + ], + 'has_matching_session': matching_session is not None, + 'matching_session': matching_session + } + + except Exception as e: + logger.error(f"Error checking existing sessions: {e}") + return { + 'url': url, + 'current_hash': current_hash, + 'existing_sessions': [], + 'has_matching_session': False, + 'matching_session': None + } + +async def create_session_document(session_data: dict) -> str: + """Create a new session document in MongoDB using hash as ID""" + session_data["_id"] = session_data["session_id"] + try: + await sessions_collection.insert_one(session_data) + logger.info(f"Created session: {session_data['session_id']}") + return session_data["session_id"] + except Exception as e: + logger.error(f"Error creating session: {e}") + raise HTTPException(status_code=500, detail="Failed to create session") + +async def update_session_document(session_id: str, update_data: dict) -> bool: + """Update session document in MongoDB""" + update_data["updated_at"] = datetime.now() + try: + result = await sessions_collection.update_one( + {"_id": session_id}, + {"$set": update_data} + ) + logger.info(f"Updated session: {session_id}") + return result.modified_count > 0 + except Exception as e: + logger.error(f"Error updating session {session_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to update session") + +async def get_session_document(session_id: str) -> dict: + """Get session document from MongoDB""" + try: + session = await sessions_collection.find_one({"_id": session_id}) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + return session + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting session {session_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to get session") + +async def get_last_session() -> Optional[dict]: + """Get the most recently updated session""" + try: + cursor = sessions_collection.find().sort("updated_at", -1).limit(1) + sessions = await cursor.to_list(length=1) + return sessions[0] if sessions else None + except Exception as e: + logger.error(f"Error getting last session: {e}") + return None + +# Mock LLM function (replace with actual LLM integration) +async def call_llm(prompt: str, context: str = "") -> str: + """Mock LLM call - replace with actual LLM integration""" + await asyncio.sleep(1) # Simulate API call delay + + if "extract elements" in prompt.lower() or "test cases" in prompt.lower(): + return json.dumps([ + {"test_name": "Login with valid credentials", "steps": ["Enter username", "Enter password", "Click submit"], "expected": "User should be logged in"}, + {"test_name": "Login with invalid credentials", "steps": ["Enter invalid username", "Enter invalid password", "Click submit"], "expected": "Error message should appear"}, + {"test_name": "Form validation", "steps": ["Leave required fields empty", "Try to submit"], "expected": "Validation errors should appear"}, + {"test_name": "Navigation test", "steps": ["Click navigation links"], "expected": "Should navigate to correct pages"} + ]) + else: + return '''import pytest +from playwright.sync_api import sync_playwright, Page, Browser +import time + +class TestWebPage: + def setup_method(self): + self.playwright = sync_playwright().start() + self.browser = self.playwright.chromium.launch(headless=False) + self.page = self.browser.new_page() + self.page.goto("''' + context.get('web_url', 'https://example.com') + '''") + + def test_page_elements_visible(self): + """Test that key elements are visible and interactable""" + try: + # Test buttons + buttons = self.page.locator('button, input[type="button"], input[type="submit"]').all() + assert len(buttons) > 0, "No buttons found on page" + + for button in buttons[:3]: # Test first 3 buttons + assert button.is_visible(), f"Button not visible: {button}" + + # Test inputs + inputs = self.page.locator('input[type="text"], input[type="email"], input[type="password"]').all() + for input_elem in inputs[:3]: # Test first 3 inputs + assert input_elem.is_visible(), f"Input not visible: {input_elem}" + + print("✓ Page elements visibility test passed") + except Exception as e: + print(f"✗ Page elements test failed: {e}") + raise + + def test_form_interactions(self): + """Test form interactions""" + try: + # Find forms + forms = self.page.locator('form').all() + if forms: + form = forms[0] + inputs = form.locator('input[type="text"], input[type="email"]').all() + + for i, input_elem in enumerate(inputs[:2]): + input_elem.fill(f"test_value_{i}") + + print("✓ Form interaction test passed") + else: + print("ℹ No forms found for testing") + except Exception as e: + print(f"✗ Form interaction test failed: {e}") + + def test_navigation_links(self): + """Test navigation links""" + try: + links = self.page.locator('a[href]').all() + working_links = 0 + + for link in links[:5]: # Test first 5 links + href = link.get_attribute('href') + if href and not href.startswith('javascript:') and not href.startswith('#'): + working_links += 1 + + assert working_links > 0, "No working navigation links found" + print(f"✓ Navigation test passed - {working_links} working links found") + except Exception as e: + print(f"✗ Navigation test failed: {e}") + + def teardown_method(self): + if hasattr(self, 'browser'): + self.browser.close() + if hasattr(self, 'playwright'): + self.playwright.stop() + +if __name__ == "__main__": + test = TestWebPage() + test.setup_method() + test.test_page_elements_visible() + test.test_form_interactions() + test.test_navigation_links() + test.teardown_method()''' + +async def execute_test_code(code: str, execution_type: str) -> Dict[str, Any]: + """Execute the generated test code""" + try: + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file: + temp_file.write(code) + temp_file_path = temp_file.name + + # Execute the code + if execution_type == "python": + result = subprocess.run( + ["python", temp_file_path], + capture_output=True, + text=True, + timeout=120 # Increased timeout for playwright + ) + elif execution_type == "pytest": + result = subprocess.run( + ["python", "-m", "pytest", temp_file_path, "-v"], + capture_output=True, + text=True, + timeout=120 + ) + else: # Default to python execution + result = subprocess.run( + ["python", temp_file_path], + capture_output=True, + text=True, + timeout=120 + ) + + # Clean up + os.unlink(temp_file_path) + + return { + "success": result.returncode == 0, + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "execution_type": execution_type + } + + except subprocess.TimeoutExpired: + return { + "success": False, + "returncode": -1, + "stdout": "", + "stderr": "Execution timed out after 120 seconds", + "execution_type": execution_type + } + except Exception as e: + return { + "success": False, + "returncode": -1, + "stdout": "", + "stderr": f"Execution error: {str(e)}", + "execution_type": execution_type + } + +# API Endpoints +@app.post("/api/check-url-hash", response_model=UrlHashCheckResponse) +async def check_url_hash(request: UrlHashCheckRequest): + """Check if URL has existing sessions and get current hash""" + try: + # Extract elements to calculate current hash + elements = await extract_elements_with_playwright(str(request.web_url)) + current_hash = calculate_url_hash(elements, str(request.web_url)) + + # Check existing sessions + session_check = await check_existing_sessions(str(request.web_url), current_hash) + + return UrlHashCheckResponse( + url=str(request.web_url), + current_hash=current_hash, + existing_sessions=session_check['existing_sessions'], + has_existing_session=session_check['has_matching_session'] + ) + + except Exception as e: + logger.error(f"URL hash check failed: {e}") + raise HTTPException(status_code=500, detail=f"URL hash check failed: {str(e)}") + +@app.post("/api/extract-elements", response_model=ElementExtractionResponse) +async def extract_elements(request: ElementExtractionRequest): + """Extract UI elements from the provided web URL""" + try: + # Extract elements using Playwright + elements = await extract_elements_with_playwright(str(request.web_url)) + url_hash = calculate_url_hash(elements, str(request.web_url)) + + # Check if we should use existing session or create new one + if not request.force_new_session: + session_check = await check_existing_sessions(str(request.web_url), url_hash) + if session_check['has_matching_session']: + existing_session = session_check['matching_session'] + return ElementExtractionResponse( + session_id=existing_session['session_id'], + url_hash=url_hash, + extracted_elements=existing_session['extracted_elements'], + timestamp=existing_session['updated_at'], + is_new_session=False + ) + + # Create new session with hash as ID + session_id = url_hash + session_data = { + "session_id": session_id, + "url_hash": url_hash, + "current_stage": "element_extraction_complete", + "completed_stages": ["element_extraction"], + "web_url": str(request.web_url), + "config_content": request.config_content, + "extracted_elements": elements, + "created_at": datetime.now(), + "updated_at": datetime.now() + } + + await create_session_document(session_data) + + return ElementExtractionResponse( + session_id=session_id, + url_hash=url_hash, + extracted_elements=elements, + timestamp=datetime.now(), + is_new_session=True + ) + + except Exception as e: + logger.error(f"Element extraction failed: {e}") + raise HTTPException(status_code=500, detail=f"Element extraction failed: {str(e)}") + +@app.post("/api/generate-testcases", response_model=TestcaseGenerationResponse) +async def generate_testcases(request: TestcaseGenerationRequest): + """Generate test cases based on extracted elements""" + session = await get_session_document(request.session_id) + + # Construct prompt for testcase generation + full_prompt = f""" + System: {request.system_prompt} + User: {request.user_prompt} + + Extracted Elements: {json.dumps(request.extracted_elements[:20])} # Limit for prompt size + {f"Sample Testcase: {request.testcase_sample}" if request.testcase_sample else ""} + + Please generate comprehensive test cases for these UI elements focusing on user interactions. + """ + + try: + # Call LLM to generate test cases + llm_response = await call_llm(full_prompt) + test_cases = json.loads(llm_response) + + # Update session document + update_data = { + "current_stage": "testcase_generation_complete", + "completed_stages": session["completed_stages"] + ["testcase_generation"], + "test_cases": test_cases + } + + await update_session_document(request.session_id, update_data) + + return TestcaseGenerationResponse( + session_id=request.session_id, + test_cases=test_cases, + timestamp=datetime.now() + ) + except Exception as e: + logger.error(f"Testcase generation failed: {e}") + raise HTTPException(status_code=500, detail=f"Testcase generation failed: {str(e)}") + +@app.post("/api/generate-code", response_model=CodeGenerationResponse) +async def generate_code(request: CodeGenerationRequest): + """Generate test code based on test cases""" + session = await get_session_document(request.session_id) + + # Construct prompt for code generation + full_prompt = f""" + System: {request.system_prompt} + User: {request.user_prompt} + + Test Cases: {json.dumps(request.test_cases)} + Web URL: {session.get('web_url', '')} + + Please generate complete, executable Playwright test code for these test cases. + """ + + try: + # Call LLM to generate code + generated_code = await call_llm(full_prompt, {'web_url': session.get('web_url', '')}) + + # Update session document + update_data = { + "current_stage": "code_generation_complete", + "completed_stages": session["completed_stages"] + ["code_generation"], + "generated_code": generated_code + } + + await update_session_document(request.session_id, update_data) + + return CodeGenerationResponse( + session_id=request.session_id, + generated_code=generated_code, + timestamp=datetime.now() + ) + except Exception as e: + logger.error(f"Code generation failed: {e}") + raise HTTPException(status_code=500, detail=f"Code generation failed: {str(e)}") + +@app.post("/api/execute-code", response_model=CodeExecutionResponse) +async def execute_code(request: CodeExecutionRequest): + """Execute the generated test code""" + session = await get_session_document(request.session_id) + + try: + # Execute the code + execution_result = await execute_test_code(request.code, request.execution_type) + + # Update session document + update_data = { + "current_stage": "code_execution_complete", + "completed_stages": session["completed_stages"] + ["code_execution"], + "execution_result": execution_result + } + + await update_session_document(request.session_id, update_data) + + return CodeExecutionResponse( + session_id=request.session_id, + execution_result=execution_result, + timestamp=datetime.now() + ) + except Exception as e: + logger.error(f"Code execution failed: {e}") + raise HTTPException(status_code=500, detail=f"Code execution failed: {str(e)}") + +@app.get("/api/session/{session_id}", response_model=SessionStatus) +async def get_session_status(session_id: str): + """Get current session status and results""" + session = await get_session_document(session_id) + + return SessionStatus( + session_id=session_id, + url_hash=session["url_hash"], + web_url=session["web_url"], + current_stage=session["current_stage"], + completed_stages=session["completed_stages"], + results={ + "extracted_elements": session.get("extracted_elements"), + "test_cases": session.get("test_cases"), + "generated_code": session.get("generated_code"), + "execution_result": session.get("execution_result") + }, + created_at=session["created_at"], + updated_at=session["updated_at"] + ) + +@app.get("/api/url-history/{url_encoded}") +async def get_url_history(url_encoded: str): + """Get session history for a specific URL""" + try: + # Decode URL + import urllib.parse + url = urllib.parse.unquote(url_encoded) + + cursor = sessions_collection.find( + {"web_url": url} + ).sort("updated_at", -1) + + sessions = await cursor.to_list(length=20) + + return { + "url": url, + "sessions": [ + { + "session_id": session["session_id"], + "url_hash": session["url_hash"], + "current_stage": session["current_stage"], + "completed_stages": session["completed_stages"], + "created_at": session["created_at"], + "updated_at": session["updated_at"], + "elements_count": len(session.get("extracted_elements", [])) + } + for session in sessions + ] + } + except Exception as e: + logger.error(f"Error getting URL history: {e}") + raise HTTPException(status_code=500, detail="Failed to get URL history") + +@app.get("/api/sessions") +async def list_sessions(): + """List all sessions""" + try: + cursor = sessions_collection.find().sort("updated_at", -1) + sessions = await cursor.to_list(length=100) + + return { + "sessions": [ + { + "session_id": session["session_id"], + "url_hash": session["url_hash"], + "current_stage": session["current_stage"], + "created_at": session["created_at"], + "updated_at": session["updated_at"], + "web_url": session.get("web_url") + } + for session in sessions + ] + } + except Exception as e: + logger.error(f"Error listing sessions: {e}") + raise HTTPException(status_code=500, detail="Failed to list sessions") + +@app.get("/api/last-session") +async def get_last_session_endpoint(): + """Get the most recently updated session""" + session = await get_last_session() + if not session: + return {"session": None} + + return { + "session": { + "session_id": session["session_id"], + "url_hash": session["url_hash"], + "current_stage": session["current_stage"], + "completed_stages": session["completed_stages"], + "results": { + "extracted_elements": session.get("extracted_elements"), + "test_cases": session.get("test_cases"), + "generated_code": session.get("generated_code"), + "execution_result": session.get("execution_result") + }, + "web_url": session.get("web_url"), + "config_content": session.get("config_content"), + "created_at": session["created_at"], + "updated_at": session["updated_at"] + } + } + +@app.delete("/api/session/{session_id}") +async def delete_session(session_id: str): + """Delete a session""" + try: + result = await sessions_collection.delete_one({"_id": session_id}) + if result.deleted_count == 0: + raise HTTPException(status_code=404, detail="Session not found") + + logger.info(f"Deleted session: {session_id}") + return {"message": "Session deleted successfully"} + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting session {session_id}: {e}") + raise HTTPException(status_code=500, detail="Failed to delete session") + +@app.get("/api/health") +async def health_check(): + """Health check endpoint""" + try: + # Test MongoDB connection + await sessions_collection.find_one({}) + return { + "status": "healthy", + "timestamp": datetime.now(), + "database": "connected", + "playwright": "ready" + } + except Exception as e: + return { + "status": "unhealthy", + "timestamp": datetime.now(), + "database": "disconnected", + "error": str(e) + } + +@app.on_event("startup") +async def startup_event(): + """Initialize database indexes on startup""" + try: + # Create indexes for better performance + await sessions_collection.create_index("session_id") + await sessions_collection.create_index("url_hash") + await sessions_collection.create_index("web_url") + await sessions_collection.create_index("updated_at") + logger.info("Database indexes created successfully") + except Exception as e: + logger.error(f"Error creating database indexes: {e}") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/frontend/fastapi_react_flow_diagram (1).md b/frontend/fastapi_react_flow_diagram (1).md new file mode 100644 index 0000000000..7c325b82fe --- /dev/null +++ b/frontend/fastapi_react_flow_diagram (1).md @@ -0,0 +1,388 @@ +# LLM UI Testing Framework - Enhanced Flow Diagram (Hash-based Sessions) + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ REACT FRONTEND │ +│ (http://localhost:3000) │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ ENHANCED UI COMPONENTS & STATE │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Step Indicator │ │ Element Form │ │ Testcase Form │ │ Code Form │ │ +│ │ ┌─┬─┬─┬─┐ │ │ • Web URL + │ │ • Enhanced │ │ • Playwright │ │ +│ │ │1│2│3│4│ │ │ History btn │ │ Prompts │ │ Code Gen │ │ +│ │ └─┴─┴─┴─┘ │ │ • Config │ │ • Test Cases │ │ • Robust │ │ +│ └──────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Execute Form │ │ Session Dialog │ │ History Modal │ │ Hash Management │ │ +│ │ • Playwright │ │ • Load Previous │ │ • URL Sessions │ │ • URL Hash │ │ +│ │ • Python/Pytest │ │ • Start Fresh │ │ • Timeline │ │ • Session ID │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────┼─────────────────────────────────────────┐ │ +│ │ ENHANCED SESSION MGMT │ RESULTS DISPLAY │ │ +│ │ • currentStep: number (0-3) │ • elements: Comprehensive JSON │ │ +│ │ • sessionId: string (URL Hash) │ • testcases: Enhanced scenarios │ │ +│ │ • urlHash: string (Content Hash) │ • code: Playwright test code │ │ +│ │ • sessionDialogData: Object │ • execution: Detailed results │ │ +│ │ • urlHistory: Array │ • element categories & metadata │ │ +│ └─────────────────────────────────────────┴─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┬───────────────────┼───────────────────┬───────────────┐ + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ POST │ │ POST │ │ POST │ │ POST │ │ GET │ │ GET │ +│/check-url- │ │/extract- │ │/generate- │ │/generate- │ │/execute- │ │/url-history/│ +│hash │ │elements │ │testcases │ │code │ │code │ │{url} │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ • Hash calc │ │ • Playwright│ │ • Enhanced │ │ • Playwright│ │ • Enhanced │ │ • Session │ +│ • Session │ │ • Elements │ │ test cases│ │ code gen │ │ execution │ │ history │ +│ check │ │ • Hash-based│ │ • Context │ │ • Context │ │ • Timeout │ │ • Timeline │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ │ │ │ + └───────────────┼───────────────────┼───────────────────┼───────────────┘ + │ │ │ + └───────────────────┼───────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ ENHANCED FASTAPI BACKEND │ +│ (http://localhost:8000) │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ ENHANCED API ENDPOINTS │ │ +│ │ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ │ +│ │ │ /check-url- │ │ /extract- │ │ /generate- │ │ /generate- │ │/execute│ │ │ +│ │ │ hash │ │ elements │ │ testcases │ │ code │ │-code │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ • Calculate │ │ • Playwright │ │ • Enhanced │ │ • Playwright │ │• 120s │ │ │ +│ │ │ hash │ │ extraction │ │ prompts │ │ code gen │ │timeout │ │ │ +│ │ │ • Find │ │ • Hash calc │ │ • Context │ │ • Context │ │• Result│ │ │ +│ │ │ existing │ │ • Session │ │ aware │ │ aware │ │detail │ │ │ +│ │ │ sessions │ │ mgmt │ │ │ │ │ │ │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ /url-history/│ │ /session/ │ │ /sessions │ │ /last- │ │ │ +│ │ │ {url} │ │ {id} │ │ │ │ session │ │ │ +│ │ │ │ │ │ │ │ │ │ │ │ +│ │ │ • URL-based │ │ • Hash-based │ │ • All │ │ • Recent │ │ │ +│ │ │ history │ │ lookup │ │ sessions │ │ session │ │ │ +│ │ │ • Timeline │ │ • Complete │ │ • Pagination │ │ • Auto-load │ │ │ +│ │ │ view │ │ data │ │ │ │ │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ ENHANCED REQUEST PROCESSING │ │ +│ │ │ │ +│ │ 1. ┌─────────────────┐ 2. ┌─────────────────┐ 3. ┌─────────────────┐ │ │ +│ │ │ URL Hash Check │ ──> │ Playwright │ ──> │ LLM/Execution │ │ │ +│ │ │ │ │ Extraction │ │ Services │ │ │ +│ │ │ • Extract with │ │ │ │ │ │ │ +│ │ │ Playwright │ │ • Comprehensive │ │ • Context-aware │ │ │ +│ │ │ • Calculate │ │ element scan │ │ prompts │ │ │ +│ │ │ content hash │ │ • Robust │ │ • Enhanced code │ │ │ +│ │ │ • Check │ │ selectors │ │ generation │ │ │ +│ │ │ existing │ │ • Categories │ │ • Secure │ │ │ +│ │ │ sessions │ │ • Metadata │ │ execution │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ │ │ │ │ │ +│ │ 4. ┌─────────────────┐ 5. ┌─────────────────┐ │ │ │ +│ │ │ Hash-based │ <── │ Process Results │ <──────────────┘ │ │ +│ │ │ Session Update │ │ │ │ │ +│ │ │ │ │ • Validate data │ │ │ +│ │ │ • Use hash as │ │ • Format output │ │ │ +│ │ │ session ID │ │ • Error │ │ │ +│ │ │ • Track UI │ │ handling │ │ │ +│ │ │ changes │ │ • Logging │ │ │ +│ │ │ • Maintain │ │ │ │ │ +│ │ │ history │ │ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ ENHANCED MONGODB DATABASE STORAGE │ │ +│ │ (mongodb://localhost:27017) │ │ +│ │ │ │ +│ │ Database: llm_testing_framework │ │ +│ │ Collection: sessions │ │ +│ │ │ │ +│ │ Enhanced Document Schema: │ │ +│ │ { │ │ +│ │ "_id": "url_content_hash_16_chars", # Hash as primary key │ │ +│ │ "session_id": "url_content_hash_16_chars", │ │ +│ │ "url_hash": "url_content_hash_16_chars", │ │ +│ │ "web_url": "https://example.com", │ │ +│ │ "current_stage": "code_execution_complete", │ │ +│ │ "completed_stages": ["element_extraction", "testcase_generation", │ │ +│ │ "code_generation", "code_execution"], │ │ +│ │ "config_content": "playwright_config...", │ │ +│ │ "extracted_elements": [ │ │ +│ │ { │ │ +│ │ "category": "buttons", │ │ +│ │ "tag_name": "button", │ │ +│ │ "text": "Submit", │ │ +│ │ "attributes": {"id": "submit-btn", "class": "btn primary"}, │ │ +│ │ "selectors": { │ │ +│ │ "css": "button#submit-btn.btn.primary", │ │ +│ │ "xpath": "//button[@id='submit-btn'][1]", │ │ +│ │ "text": "text='Submit'", │ │ +│ │ "id": "[id='submit-btn']" │ │ +│ │ }, │ │ +│ │ "bounding_box": {"x": 100, "y": 200, "width": 80, "height": 30}, │ │ +│ │ "is_visible": true, │ │ +│ │ "is_enabled": true, │ │ +│ │ "element_type": "button" │ │ +│ │ }, │ │ +│ │ ... more elements ..., │ │ +│ │ { │ │ +│ │ "type": "metadata", │ │ +│ │ "data": { │ │ +│ │ "total_elements": 45, │ │ +│ │ "extraction_timestamp": "2025-06-08T...", │ │ +│ │ "url": "https://example.com", │ │ +│ │ "screenshot": "base64_truncated...", │ │ +│ │ "categories": { │ │ +│ │ "buttons": 8, "inputs": 12, "links": 15, │ │ +│ │ "forms": 2, "selects": 3, "checkboxes": 5 │ │ +│ │ } │ │ +│ │ } │ │ +│ │ } │ │ +│ │ ], │ │ +│ │ "test_cases": [ │ │ +│ │ { │ │ +│ │ "test_name": "Enhanced form submission test", │ │ +│ │ "steps": ["Fill form fields", "Validate inputs", "Submit form"], │ │ +│ │ "expected": "Form should submit successfully with validation" │ │ +│ │ } │ │ +│ │ ], │ │ +│ │ "generated_code": "import pytest\nfrom playwright.sync_api import...", │ │ +│ │ "execution_result": { │ │ +│ │ "success": true, │ │ +│ │ "returncode": 0, │ │ +│ │ "stdout": "Playwright test execution output...", │ │ +│ │ "stderr": "", │ │ +│ │ "execution_type": "python" │ │ +│ │ }, │ │ +│ │ "created_at": "2025-06-08T...", │ │ +│ │ "updated_at": "2025-06-08T..." │ │ +│ │ } │ │ +│ │ │ │ +│ │ Enhanced Indexes: session_id, url_hash, web_url, updated_at │ │ +│ │ Motor AsyncIO Client for high-performance async operations │ │ +│ │ URL-based session grouping for historical tracking │ │ +│ └─────────────────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ ENHANCED PLAYWRIGHT & LLM SERVICES │ +│ (Comprehensive Extraction + Code Generation) │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ PLAYWRIGHT ELEMENT EXTRACTION ENGINE │ │ +│ │ │ │ +│ │ async def extract_elements_with_playwright(url, config): │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Browser Launch │ │ Comprehensive │ │ Element Data │ │ │ +│ │ │ │ │ Element Scan │ │ Extraction │ │ │ +│ │ │ • Chromium │ │ │ │ │ │ │ +│ │ │ • No sandbox │ │ • Buttons │ │ • Tag name │ │ │ +│ │ │ • 1920x1080 │ │ • Inputs (all) │ │ • Text content │ │ │ +│ │ │ • User agent │ │ • Selects │ │ • Attributes │ │ │ +│ │ │ • 30s timeout │ │ • Checkboxes │ │ • Bounding box │ │ │ +│ │ │ │ │ • Radios │ │ • Visibility │ │ │ +│ │ │ │ │ • Links │ │ • Enabled state │ │ │ +│ │ │ │ │ • Interactive │ │ • Element type │ │ │ +│ │ │ │ │ • Forms │ │ │ │ │ +│ │ │ │ │ • Images │ │ │ │ │ +│ │ │ │ │ • Headings │ │ │ │ │ +│ │ │ │ │ • Landmarks │ │ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Robust Selector │ │ Duplicate │ │ Hash Calculation│ │ │ +│ │ │ Generation │ │ Removal │ │ │ │ │ +│ │ │ │ │ │ │ • URL structure │ │ │ +│ │ │ • CSS selector │ │ • Position- │ │ • Element count │ │ │ +│ │ │ • XPath │ │ based │ │ • Structural │ │ │ +│ │ │ • Text-based │ │ • Selector- │ │ fingerprint │ │ │ +│ │ │ • ID-based │ │ based │ │ • Content hash │ │ │ +│ │ │ • Name-based │ │ • Unique key │ │ • SHA256 (16) │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ ENHANCED LLM SERVICE INTEGRATION │ │ +│ │ │ │ +│ │ async def call_llm(prompt, context): │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Test Case │ │ Playwright Code │ │ Context-Aware │ │ │ +│ │ │ Generation │ │ Generation │ │ Processing │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ Returns: │ │ Returns: │ │ • Element │ │ │ +│ │ │ Enhanced JSON │ │ Complete │ │ categories │ │ │ +│ │ │ test scenarios │ │ Playwright code │ │ • URL context │ │ │ +│ │ │ │ │ │ │ • Test focus │ │ │ +│ │ │ [ │ │ import pytest │ │ • Robust │ │ │ +│ │ │ { │ │ from playwright │ │ selectors │ │ │ +│ │ │ "test_name":│ │ ... │ │ • Error │ │ │ +│ │ │ "steps": │ │ class TestPage: │ │ handling │ │ │ +│ │ │ "expected": │ │ def setup(): │ │ │ │ │ +│ │ │ "priority": │ │ def test_(): │ │ │ │ │ +│ │ │ } │ │ def teardown()│ │ │ │ │ +│ │ │ ] │ │ │ │ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ ENHANCED CODE EXECUTION ENGINE │ │ +│ │ │ │ +│ │ async def execute_test_code(code, execution_type): │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Secure File │ │ Enhanced │ │ Detailed │ │ │ +│ │ │ Handling │ │ Execution │ │ Results │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ • Temp file │ │ subprocess.run( │ │ { │ │ │ +│ │ │ creation │ │ ["python", │ │ "success": │ │ │ +│ │ │ • Write code │ │ temp_file], │ │ "returncode": │ │ │ +│ │ │ • Permission │ │ capture=True, │ │ "stdout": │ │ │ +│ │ │ handling │ │ timeout=120 │ │ "stderr": │ │ │ +│ │ │ • Auto cleanup │ │ ) │ │ "exec_type": │ │ │ +│ │ │ │ │ │ │ "timestamp": │ │ │ +│ │ │ │ │ Types: │ │ } │ │ │ +│ │ │ │ │ • python │ │ │ │ │ +│ │ │ │ │ • pytest │ │ │ │ │ +│ │ │ │ │ • playwright │ │ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ │ │ │ +│ │ Security: 120s timeout, temp file cleanup, sandboxed subprocess execution │ │ +│ │ Error handling: Timeout, permission, execution errors with detailed logging │ │ +│ └─────────────────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ ENHANCED DATA FLOW SUMMARY │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. INTELLIGENT SESSION MANAGEMENT │ +│ │ │ +│ ├─► URL Input & Hash Check │ +│ │ • User enters URL │ +│ │ • POST /api/check-url-hash │ +│ │ • Playwright extracts elements comprehensively │ +│ │ • Calculate content-based hash (SHA256) │ +│ │ • Check existing sessions for URL │ +│ │ • Return hash + existing session list │ +│ │ │ +│ ├─► Smart Session Dialog │ +│ │ • If existing sessions found, show dialog │ +│ │ • User can choose: Load Previous or Start Fresh │ +│ │ • Display session metadata (stages, dates, elements count) │ +│ │ • Hash-based session identification │ +│ │ │ +│ 2. ENHANCED 4-STEP WORKFLOW │ +│ │ │ +│ ├─► Step 1: Enhanced Element Extraction │ +│ │ • Playwright comprehensive scan (10+ element types) │ +│ │ • Robust selector generation (CSS, XPath, Text, ID) │ +│ │ • Element categorization and metadata │ +│ │ • Content hash calculation for session ID │ +│ │ • Store in MongoDB with hash as primary key │ +│ │ │ +│ ├─► Step 2: Context-Aware Test Case Generation │ +│ │ • Enhanced prompts with element context │ +│ │ • Comprehensive test scenarios (positive/negative/edge) │ +│ │ • User workflow and interaction testing │ +│ │ • Priority-based test case organization │ +│ │ │ +│ ├─► Step 3: Playwright Code Generation │ +│ │ • Generate production-ready Playwright test code │ +│ │ • Robust error handling and assertions │ +│ │ • Context-aware selectors from extraction │ +│ │ • Setup, teardown, and configuration management │ +│ │ │ +│ └─► Step 4: Enhanced Code Execution │ +│ • Support for Python, Pytest, Playwright execution │ +│ • 120-second timeout for complex tests │ +│ • Detailed execution results with stdout/stderr │ +│ • Secure temporary file handling │ +│ │ +│ 3. INTELLIGENT HISTORY & VERSION TRACKING │ +│ • Hash-based session identification enables UI change tracking │ +│ • URL history shows evolution of page structure over time │ +│ • Historical sessions preserved for comparison and analysis │ +│ • Timeline view of testing sessions for each URL │ +│ │ +│ 4. PRODUCTION-READY FEATURES │ +│ • MongoDB with optimized indexes for hash and URL-based queries │ +│ • Playwright browser automation with comprehensive element detection │ +│ • Secure code execution with timeout and sandboxing │ +│ • Enhanced error handling and detailed logging │ +│ • Session restoration and state management │ +│ • Responsive UI with loading states and progress indicators │ +│ │ +│ 5. ADDITIONAL ENHANCED ENDPOINTS │ +│ • POST /api/check-url-hash - Intelligent session detection │ +│ • GET /api/url-history/{url} - URL-specific session history │ +│ • GET /api/session/{hash} - Hash-based session retrieval │ +│ • Enhanced session management with automated cleanup │ +│ • Health checks including Playwright and database connectivity │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ ENHANCED TECHNOLOGY STACK & FEATURES │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ FRONTEND (Enhanced React) │ BACKEND (Enhanced FastAPI) │ +│ ├─ React 18 with advanced hooks │ ├─ FastAPI with async/await │ +│ ├─ Smart session management │ ├─ Playwright for element extraction │ +│ ├─ Hash-based session detection │ ├─ Hash-based session management │ +│ ├─ URL history and timeline │ ├─ Motor AsyncIO MongoDB driver │ +│ ├─ Session loading dialogs │ ├─ Enhanced subprocess execution │ +│ ├─ Enhanced error handling │ ├─ Comprehensive element categorization │ +│ ├─ Real-time progress indicators │ ├─ Robust selector generation │ +│ ├─ Element category visualization │ ├─ Context-aware LLM integration │ +│ ├─ Copy-to-clipboard functionality │ ├─ Content-based hash calculation │ +│ ├─ Responsive design with modals │ ├─ URL-based session grouping │ +│ └─ Auto-session restoration │ └─ Enhanced logging and monitoring │ +│ │ │ +│ DATABASE (Enhanced MongoDB) │ SECURITY & EXECUTION (Enhanced) │ +│ ├─ Hash-based primary keys │ ├─ Playwright browser sandboxing │ +│ ├─ URL-based session grouping │ ├─ 120-second execution timeout │ +│ ├─ Element metadata storage │ ├─ Secure temporary file handling │ +│ ├─ Comprehensive indexing │ ├─ Input validation with enhanced Pydantic │ +│ ├─ Historical session tracking │ ├─ Detailed error logging and monitoring │ +│ ├─ Optimized query performance │ ├─ Database connection health monitoring │ +│ └─ Automatic cleanup policies │ └─ Hash-based session security │ +│ │ │ +│ DEPENDENCIES (Enhanced) │ INSTALLATION (Enhanced) │ +│ Frontend: │ Backend: │ +│ ├─ react, lucide-react │ ├─ pip install fastapi uvicorn │ +│ ├─ tailwindcss │ ├─ pip install playwright motor pymongo │ +│ └─ Enhanced state management │ ├─ pip install pydantic python-multipart │ +│ │ ├─ playwright install chromium │ +│ Backend (Enhanced): │ └─ MongoDB server with optimized config │ +│ ├─ fastapi, uvicorn │ │ +│ ├─ playwright (async) │ MongoDB (Enhanced): │ +│ ├─ motor, pymongo │ ├─ Default port: 27017 │ +│ ├─ pydantic, python-multipart │ ├─ Database: llm_testing_framework │ +│ └─ hashlib, base64, logging │ ├─ Collection: sessions (hash-indexed) │ +│ │ └─ Optimized indexes for performance │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ +``` \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 57621a268b..de26edfa2a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,14 +1,52 @@ - - - - - - - Full Stack FastAPI Project - - - -
- - - + + + + + + Employee Timesheet Dashboard + + + + + +
+

Employee Timesheet Dashboard

+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + + + \ No newline at end of file diff --git a/frontend/react_frontend.tsx b/frontend/react_frontend.tsx new file mode 100644 index 0000000000..123b517ccf --- /dev/null +++ b/frontend/react_frontend.tsx @@ -0,0 +1,786 @@ +import React, { useState, useEffect } from 'react'; +import { Play, Code, FileText, Globe, CheckCircle, Circle, ArrowRight, Download, Copy, RefreshCw, Terminal, AlertCircle, History, ExternalLink, X } from 'lucide-react'; + +const API_BASE_URL = 'http://localhost:8000/api'; + +const TestingFrameworkApp = () => { + const [currentStep, setCurrentStep] = useState(0); + const [sessionId, setSessionId] = useState(null); + const [urlHash, setUrlHash] = useState(null); + const [loading, setLoading] = useState(false); + const [initialLoading, setInitialLoading] = useState(true); + const [showSessionDialog, setShowSessionDialog] = useState(false); + const [sessionDialogData, setSessionDialogData] = useState(null); + const [showHistoryModal, setShowHistoryModal] = useState(false); + const [urlHistory, setUrlHistory] = useState([]); + const [results, setResults] = useState({ + elements: null, + testcases: null, + code: null, + execution: null + }); + + // Form states + const [elementForm, setElementForm] = useState({ + webUrl: '', + configContent: '', + systemPrompt: 'You are an expert UI element extraction assistant. Analyze the webpage and extract all interactive elements comprehensively.', + userPrompt: 'Please extract all UI elements from this webpage including buttons, inputs, links, forms, and other interactive components. Use robust selectors and comprehensive extraction strategies.' + }); + + const [testcaseForm, setTestcaseForm] = useState({ + systemPrompt: 'You are an expert test case generation assistant. Create comprehensive test scenarios based on extracted UI elements.', + userPrompt: 'Generate detailed test cases for the extracted UI elements. Include positive, negative, edge case scenarios, and user workflow tests. Focus on real-world user interactions.' + }); + + const [codeForm, setCodeForm] = useState({ + systemPrompt: 'You are an expert test automation engineer. Generate clean, executable Playwright test code.', + userPrompt: 'Generate complete Playwright test code for the provided test cases. Include proper setup, teardown, error handling, and comprehensive assertions. Make the tests robust and maintainable.' + }); + + const [executeForm, setExecuteForm] = useState({ + executionType: 'python' + }); + + const steps = [ + { title: 'Element Extraction', icon: Globe, description: 'Extract UI elements using Playwright' }, + { title: 'Test Cases Generation', icon: FileText, description: 'Generate comprehensive test scenarios' }, + { title: 'Code Generation', icon: Code, description: 'Generate executable Playwright test code' }, + { title: 'Code Execution', icon: Terminal, description: 'Execute and validate tests' } + ]; + + // Load last session on component mount + useEffect(() => { + const loadLastSession = async () => { + try { + const response = await fetch(`${API_BASE_URL}/last-session`); + if (response.ok) { + const data = await response.json(); + if (data.session) { + await loadSession(data.session); + } + } + } catch (error) { + console.error('Failed to load last session:', error); + } finally { + setInitialLoading(false); + } + }; + + loadLastSession(); + }, []); + + const loadSession = async (session) => { + setSessionId(session.session_id); + setUrlHash(session.url_hash); + + // Restore form data + if (session.web_url) { + setElementForm(prev => ({ + ...prev, + webUrl: session.web_url, + configContent: session.config_content || '' + })); + } + + // Restore results + setResults({ + elements: session.results.extracted_elements, + testcases: session.results.test_cases, + code: session.results.generated_code, + execution: session.results.execution_result + }); + + // Set current step based on completed stages + const completedStages = session.completed_stages; + if (completedStages.includes('code_execution')) { + setCurrentStep(3); + } else if (completedStages.includes('code_generation')) { + setCurrentStep(3); + } else if (completedStages.includes('testcase_generation')) { + setCurrentStep(2); + } else if (completedStages.includes('element_extraction')) { + setCurrentStep(1); + } + }; + + const callAPI = async (endpoint, data) => { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || `API call failed: ${response.statusText}`); + } + + return response.json(); + }; + + const checkUrlHash = async (url) => { + try { + const response = await callAPI('/check-url-hash', { web_url: url }); + return response; + } catch (error) { + console.error('Failed to check URL hash:', error); + throw error; + } + }; + + const handleUrlCheck = async () => { + if (!elementForm.webUrl) return; + + setLoading(true); + try { + const hashData = await checkUrlHash(elementForm.webUrl); + + if (hashData.has_existing_session || hashData.existing_sessions.length > 0) { + setSessionDialogData(hashData); + setShowSessionDialog(true); + } else { + // No existing sessions, proceed directly + await handleElementExtraction(true); + } + } catch (error) { + alert(`Error checking URL: ${error.message}`); + } + setLoading(false); + }; + + const handleElementExtraction = async (forceNew = false) => { + setLoading(true); + setShowSessionDialog(false); + + try { + const response = await callAPI('/extract-elements', { + web_url: elementForm.webUrl, + config_content: elementForm.configContent, + system_prompt: elementForm.systemPrompt, + user_prompt: elementForm.userPrompt, + force_new_session: forceNew + }); + + setSessionId(response.session_id); + setUrlHash(response.url_hash); + setResults(prev => ({ ...prev, elements: response.extracted_elements })); + + if (response.is_new_session) { + setCurrentStep(1); + } else { + // Loaded existing session, determine current step + const session = await fetch(`${API_BASE_URL}/session/${response.session_id}`) + .then(res => res.json()); + await loadSession(session); + } + } catch (error) { + alert(`Error: ${error.message}`); + } + setLoading(false); + }; + + const handleLoadExistingSession = async (session) => { + setLoading(true); + setShowSessionDialog(false); + + try { + const sessionData = await fetch(`${API_BASE_URL}/session/${session.session_id}`) + .then(res => res.json()); + await loadSession(sessionData); + } catch (error) { + alert(`Error loading session: ${error.message}`); + } + setLoading(false); + }; + + const handleTestcaseGeneration = async () => { + setLoading(true); + try { + const response = await callAPI('/generate-testcases', { + session_id: sessionId, + extracted_elements: results.elements, + system_prompt: testcaseForm.systemPrompt, + user_prompt: testcaseForm.userPrompt + }); + + setResults(prev => ({ ...prev, testcases: response.test_cases })); + setCurrentStep(2); + } catch (error) { + alert(`Error: ${error.message}`); + } + setLoading(false); + }; + + const handleCodeGeneration = async () => { + setLoading(true); + try { + const response = await callAPI('/generate-code', { + session_id: sessionId, + test_cases: results.testcases, + system_prompt: codeForm.systemPrompt, + user_prompt: codeForm.userPrompt + }); + + setResults(prev => ({ ...prev, code: response.generated_code })); + setCurrentStep(3); + } catch (error) { + alert(`Error: ${error.message}`); + } + setLoading(false); + }; + + const handleCodeExecution = async () => { + setLoading(true); + try { + const response = await callAPI('/execute-code', { + session_id: sessionId, + code: results.code, + execution_type: executeForm.executionType + }); + + setResults(prev => ({ ...prev, execution: response.execution_result })); + } catch (error) { + alert(`Error: ${error.message}`); + } + setLoading(false); + }; + + const loadUrlHistory = async () => { + if (!elementForm.webUrl) return; + + try { + const encodedUrl = encodeURIComponent(elementForm.webUrl); + const response = await fetch(`${API_BASE_URL}/url-history/${encodedUrl}`); + const data = await response.json(); + setUrlHistory(data.sessions || []); + setShowHistoryModal(true); + } catch (error) { + console.error('Failed to load URL history:', error); + } + }; + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text); + alert('Copied to clipboard!'); + }; + + const resetWorkflow = () => { + setCurrentStep(0); + setSessionId(null); + setUrlHash(null); + setResults({ elements: null, testcases: null, code: null, execution: null }); + setElementForm(prev => ({ ...prev, webUrl: '', configContent: '' })); + }; + + const SessionDialog = () => { + if (!showSessionDialog || !sessionDialogData) return null; + + return ( +
+
+
+

Existing Sessions Found

+ +
+ +

+ Found {sessionDialogData.existing_sessions.length} existing session(s) for this URL. + {sessionDialogData.has_existing_session && " The current page structure matches an existing session."} +

+ +
+ {sessionDialogData.existing_sessions.slice(0, 5).map((session) => ( +
+
+
+
Hash: {session.url_hash}
+
+ Stage: {session.current_stage} | + Steps: {session.completed_stages.join(', ')} +
+
+ Created: {new Date(session.created_at).toLocaleDateString()} | + Updated: {new Date(session.updated_at).toLocaleDateString()} +
+
+ +
+
+ ))} +
+ +
+ + +
+
+
+ ); + }; + + const HistoryModal = () => { + if (!showHistoryModal) return null; + + return ( +
+
+
+

URL Testing History

+ +
+ +
+ {urlHistory.map((session) => ( +
+
+
+
Hash: {session.url_hash}
+
+ Stage: {session.current_stage} | + Elements: {session.elements_count} | + Steps: {session.completed_stages.join(', ')} +
+
+ Created: {new Date(session.created_at).toLocaleString()} | + Updated: {new Date(session.updated_at).toLocaleString()} +
+
+ +
+
+ ))} +
+
+
+ ); + }; + + const StepIndicator = () => ( +
+ {steps.map((step, index) => { + const Icon = step.icon; + const isActive = index === currentStep; + const isCompleted = index < currentStep || (index === currentStep && results[Object.keys(results)[index]]); + + return ( +
+
+ + {step.title} + {isCompleted && index !== currentStep && } +
+ {index < steps.length - 1 && ( + + )} +
+ ); + })} +
+ ); + + const ElementExtractionStep = () => ( +
+

+ + Element Extraction with Playwright +

+ +
+
+ +
+ setElementForm(prev => ({ ...prev, webUrl: e.target.value }))} + className="flex-1 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="https://example.com" + required + /> + +
+
+ +
+ +