fagun-browser-testing / src /utils /advanced_error_handler.py
fagun18's picture
Upload folder using huggingface_hub
2b89d68 verified
raw
history blame
19.6 kB
"""
🤖 Fagun Browser Automation Testing Agent - Advanced Error Handler
=================================================================
Advanced error handling, debugging, and intelligent error recovery system.
Author: Mejbaur Bahar Fagun
Role: Software Engineer in Test
LinkedIn: https://www.linkedin.com/in/mejbaur/
"""
import asyncio
import logging
import traceback
import json
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass
from datetime import datetime
from playwright.async_api import Page, Locator, Error as PlaywrightError
logger = logging.getLogger(__name__)
@dataclass
class DetailedError:
"""Detailed error information with context."""
error_type: str
error_message: str
element_info: Dict[str, Any]
page_context: Dict[str, Any]
action_context: Dict[str, Any]
suggested_fix: str
severity: str # low, medium, high, critical
timestamp: datetime
stack_trace: str
class AdvancedErrorHandler:
"""Advanced error handling and debugging system."""
def __init__(self, page: Page):
self.page = page
self.error_history: List[DetailedError] = []
self.recovery_attempts: Dict[str, int] = {}
async def handle_action_error(self, error: Exception, action: str,
element_index: Optional[int] = None,
element_selector: Optional[str] = None,
input_value: Optional[str] = None) -> DetailedError:
"""Handle action errors with detailed context and suggestions."""
# Extract element information
element_info = await self._extract_element_info(element_index, element_selector)
# Extract page context
page_context = await self._extract_page_context()
# Extract action context
action_context = {
"action": action,
"element_index": element_index,
"element_selector": element_selector,
"input_value": input_value,
"timestamp": datetime.now().isoformat()
}
# Analyze error and generate suggestions
error_analysis = self._analyze_error(error, element_info, page_context, action_context)
# Create detailed error
detailed_error = DetailedError(
error_type=type(error).__name__,
error_message=str(error),
element_info=element_info,
page_context=page_context,
action_context=action_context,
suggested_fix=error_analysis["suggested_fix"],
severity=error_analysis["severity"],
timestamp=datetime.now(),
stack_trace=traceback.format_exc()
)
self.error_history.append(detailed_error)
# Log detailed error
self._log_detailed_error(detailed_error)
return detailed_error
async def _extract_element_info(self, element_index: Optional[int],
element_selector: Optional[str]) -> Dict[str, Any]:
"""Extract detailed information about the element that caused the error."""
element_info = {
"index": element_index,
"selector": element_selector,
"exists": False,
"visible": False,
"enabled": False,
"tag_name": None,
"attributes": {},
"text_content": None,
"bounding_box": None,
"similar_elements": []
}
try:
if element_index is not None:
# Get element by index
elements = await self.page.locator("input, button, select, textarea").all()
if 0 <= element_index < len(elements):
element = elements[element_index]
element_info.update(await self._get_element_details(element))
# Find similar elements
element_info["similar_elements"] = await self._find_similar_elements(element)
elif element_selector:
# Get element by selector
element = self.page.locator(element_selector).first
if await element.count() > 0:
element_info.update(await self._get_element_details(element))
element_info["similar_elements"] = await self._find_similar_elements(element)
except Exception as e:
element_info["extraction_error"] = str(e)
return element_info
async def _get_element_details(self, element: Locator) -> Dict[str, Any]:
"""Get detailed information about a specific element."""
details = {}
try:
details["exists"] = await element.count() > 0
if details["exists"]:
details["visible"] = await element.is_visible()
details["enabled"] = await element.is_enabled()
details["tag_name"] = await element.evaluate("el => el.tagName")
details["text_content"] = await element.text_content()
# Get attributes
attributes = await element.evaluate("el => { const attrs = {}; for (let attr of el.attributes) { attrs[attr.name] = attr.value; } return attrs; }")
details["attributes"] = attributes
# Get bounding box
try:
bbox = await element.bounding_box()
details["bounding_box"] = bbox
except:
details["bounding_box"] = None
except Exception as e:
details["error"] = str(e)
return details
async def _find_similar_elements(self, target_element: Locator) -> List[Dict[str, Any]]:
"""Find similar elements on the page."""
similar_elements = []
try:
# Get all interactive elements
all_elements = await self.page.locator("input, button, select, textarea, a").all()
for i, element in enumerate(all_elements):
try:
element_details = await self._get_element_details(element)
if element_details.get("exists"):
similar_elements.append({
"index": i,
"tag_name": element_details.get("tag_name"),
"attributes": element_details.get("attributes", {}),
"text_content": element_details.get("text_content"),
"visible": element_details.get("visible", False)
})
except:
continue
except Exception as e:
similar_elements.append({"error": str(e)})
return similar_elements[:10] # Limit to 10 similar elements
async def _extract_page_context(self) -> Dict[str, Any]:
"""Extract current page context information."""
context = {
"url": None,
"title": None,
"form_count": 0,
"input_count": 0,
"button_count": 0,
"page_loaded": False,
"viewport_size": None,
"user_agent": None
}
try:
context["url"] = self.page.url
context["title"] = await self.page.title()
context["form_count"] = await self.page.locator("form").count()
context["input_count"] = await self.page.locator("input").count()
context["button_count"] = await self.page.locator("button").count()
context["page_loaded"] = True
# Get viewport size
viewport = await self.page.viewport_size()
if viewport:
context["viewport_size"] = f"{viewport['width']}x{viewport['height']}"
# Get user agent
context["user_agent"] = await self.page.evaluate("navigator.userAgent")
except Exception as e:
context["extraction_error"] = str(e)
return context
def _analyze_error(self, error: Exception, element_info: Dict[str, Any],
page_context: Dict[str, Any], action_context: Dict[str, Any]) -> Dict[str, Any]:
"""Analyze error and provide intelligent suggestions."""
error_message = str(error).lower()
error_type = type(error).__name__
# Determine severity
severity = "medium"
if "timeout" in error_message or "waiting" in error_message:
severity = "high"
elif "not found" in error_message or "not visible" in error_message:
severity = "high"
elif "permission" in error_message or "blocked" in error_message:
severity = "critical"
elif "network" in error_message or "connection" in error_message:
severity = "high"
# Generate suggestions based on error type
suggested_fix = self._generate_suggestions(error_message, error_type, element_info, page_context, action_context)
return {
"severity": severity,
"suggested_fix": suggested_fix
}
def _generate_suggestions(self, error_message: str, error_type: str,
element_info: Dict[str, Any], page_context: Dict[str, Any],
action_context: Dict[str, Any]) -> str:
"""Generate intelligent suggestions for fixing the error."""
suggestions = []
# Element not found errors
if "not found" in error_message or "no element" in error_message:
suggestions.append("🔍 Element not found. Try these solutions:")
suggestions.append(" • Wait for the page to fully load")
suggestions.append(" • Check if the element selector is correct")
suggestions.append(" • Verify the element exists on the current page")
if element_info.get("similar_elements"):
suggestions.append(" • Similar elements found on page:")
for elem in element_info["similar_elements"][:3]:
if elem.get("tag_name"):
suggestions.append(f" - {elem['tag_name']}: {elem.get('text_content', 'No text')[:50]}")
# Element not visible errors
elif "not visible" in error_message or "not attached" in error_message:
suggestions.append("👁️ Element not visible. Try these solutions:")
suggestions.append(" • Scroll to the element first")
suggestions.append(" • Wait for animations to complete")
suggestions.append(" • Check if element is hidden by CSS")
suggestions.append(" • Try clicking a parent element first")
# Input text errors
elif "input text" in error_message or "failed to input" in error_message:
suggestions.append("⌨️ Input text failed. Try these solutions:")
suggestions.append(" • Clear the field first")
suggestions.append(" • Check if the field is enabled")
suggestions.append(" • Verify the field accepts text input")
suggestions.append(" • Try typing character by character")
if action_context.get("input_value"):
suggestions.append(f" • Input value was: '{action_context['input_value']}'")
# Click errors
elif "click" in error_message or "failed to click" in error_message:
suggestions.append("🖱️ Click failed. Try these solutions:")
suggestions.append(" • Wait for element to be clickable")
suggestions.append(" • Check if element is covered by another element")
suggestions.append(" • Try force clicking")
suggestions.append(" • Scroll to element before clicking")
# Timeout errors
elif "timeout" in error_message or "waiting" in error_message:
suggestions.append("⏰ Timeout occurred. Try these solutions:")
suggestions.append(" • Increase wait timeout")
suggestions.append(" • Check if page is still loading")
suggestions.append(" • Verify network connection")
suggestions.append(" • Try refreshing the page")
# Network errors
elif "network" in error_message or "connection" in error_message:
suggestions.append("🌐 Network error. Try these solutions:")
suggestions.append(" • Check internet connection")
suggestions.append(" • Verify the website is accessible")
suggestions.append(" • Try again in a few moments")
suggestions.append(" • Check for firewall/proxy issues")
# Generic suggestions
if not suggestions:
suggestions.append("🔧 General troubleshooting:")
suggestions.append(" • Refresh the page and try again")
suggestions.append(" • Check browser console for errors")
suggestions.append(" • Verify the website is working properly")
suggestions.append(" • Try a different approach or element")
# Add context-specific suggestions
if element_info.get("index") is not None:
suggestions.append(f" • Element index: {element_info['index']}")
if page_context.get("input_count", 0) > 0:
suggestions.append(f" • Page has {page_context['input_count']} input elements")
return "\n".join(suggestions)
def _log_detailed_error(self, error: DetailedError):
"""Log detailed error information."""
logger.error(f"🚨 Detailed Error Report:")
logger.error(f" Type: {error.error_type}")
logger.error(f" Message: {error.error_message}")
logger.error(f" Severity: {error.severity}")
logger.error(f" Element Index: {error.element_info.get('index', 'N/A')}")
logger.error(f" Element Selector: {error.element_info.get('selector', 'N/A')}")
logger.error(f" Element Exists: {error.element_info.get('exists', 'N/A')}")
logger.error(f" Element Visible: {error.element_info.get('visible', 'N/A')}")
logger.error(f" Page URL: {error.page_context.get('url', 'N/A')}")
logger.error(f" Page Title: {error.page_context.get('title', 'N/A')}")
logger.error(f" Suggested Fix:\n{error.suggested_fix}")
if error.severity in ["high", "critical"]:
logger.error(f" Stack Trace:\n{error.stack_trace}")
async def attempt_error_recovery(self, error: DetailedError) -> bool:
"""Attempt to recover from an error automatically."""
recovery_key = f"{error.error_type}_{error.action_context.get('action', 'unknown')}"
# Limit recovery attempts
if self.recovery_attempts.get(recovery_key, 0) >= 3:
logger.warning(f"⚠️ Maximum recovery attempts reached for {recovery_key}")
return False
self.recovery_attempts[recovery_key] = self.recovery_attempts.get(recovery_key, 0) + 1
try:
# Recovery strategies based on error type
if "not found" in error.error_message.lower():
return await self._recover_element_not_found(error)
elif "not visible" in error.error_message.lower():
return await self._recover_element_not_visible(error)
elif "input text" in error.error_message.lower():
return await self._recover_input_text_error(error)
elif "timeout" in error.error_message.lower():
return await self._recover_timeout_error(error)
except Exception as recovery_error:
logger.error(f"❌ Recovery attempt failed: {recovery_error}")
return False
async def _recover_element_not_found(self, error: DetailedError) -> bool:
"""Recover from element not found error."""
try:
# Wait a bit for dynamic content
await asyncio.sleep(2)
# Try to find similar elements
if error.element_info.get("similar_elements"):
for elem in error.element_info["similar_elements"][:3]:
if elem.get("visible") and elem.get("tag_name"):
logger.info(f"🔄 Trying similar element: {elem['tag_name']}")
return True
return False
except:
return False
async def _recover_element_not_visible(self, error: DetailedError) -> bool:
"""Recover from element not visible error."""
try:
# Scroll to element
if error.element_info.get("index") is not None:
elements = await self.page.locator("input, button, select, textarea").all()
if 0 <= error.element_info["index"] < len(elements):
await elements[error.element_info["index"]].scroll_into_view_if_needed()
await asyncio.sleep(1)
return True
return False
except:
return False
async def _recover_input_text_error(self, error: DetailedError) -> bool:
"""Recover from input text error."""
try:
# Clear field first
if error.element_info.get("index") is not None:
elements = await self.page.locator("input, textarea").all()
if 0 <= error.element_info["index"] < len(elements):
await elements[error.element_info["index"]].clear()
await asyncio.sleep(0.5)
return True
return False
except:
return False
async def _recover_timeout_error(self, error: DetailedError) -> bool:
"""Recover from timeout error."""
try:
# Wait longer
await asyncio.sleep(5)
return True
except:
return False
def get_error_summary(self) -> Dict[str, Any]:
"""Get summary of all errors encountered."""
if not self.error_history:
return {"message": "No errors recorded"}
error_counts = {}
severity_counts = {}
for error in self.error_history:
error_type = error.error_type
severity = error.severity
error_counts[error_type] = error_counts.get(error_type, 0) + 1
severity_counts[severity] = severity_counts.get(severity, 0) + 1
return {
"total_errors": len(self.error_history),
"error_types": error_counts,
"severity_distribution": severity_counts,
"recovery_attempts": self.recovery_attempts,
"recent_errors": [
{
"type": e.error_type,
"message": e.error_message,
"severity": e.severity,
"timestamp": e.timestamp.isoformat(),
"suggested_fix": e.suggested_fix
}
for e in self.error_history[-5:] # Last 5 errors
]
}