"""Dynamic checks using Playwright.""" import asyncio import json import re from typing import Any from playwright.async_api import async_playwright from shared.logger import setup_logger logger = setup_logger(__name__) class DynamicChecker: """Perform dynamic checks using Playwright.""" def __init__(self, pages_url: str, checks: list[str]) -> None: """Initialize dynamic checker. Args: pages_url: GitHub Pages URL checks: List of check descriptions """ self.pages_url = pages_url self.checks = checks async def run_checks(self) -> list[dict[str, Any]]: """Run all dynamic checks. Returns: List of check results """ results = [] async with async_playwright() as p: browser = await p.chromium.launch() page = await browser.new_page() try: # Load page await page.goto(self.pages_url, timeout=30000, wait_until="networkidle") # Wait a bit for JS to execute await page.wait_for_timeout(2000) # Run each check for check in self.checks: result = await self._run_single_check(page, check) results.append(result) except Exception as e: logger.error(f"Error loading page {self.pages_url}: {e}") results.append( { "check": "Page Load", "passed": False, "score": 0.0, "reason": f"Failed to load page: {e}", } ) finally: await browser.close() return results async def _run_single_check(self, page, check_desc: str) -> dict[str, Any]: """Run a single check. Args: page: Playwright page check_desc: Check description Returns: Check result """ try: # Parse check type if check_desc.startswith("js:"): # JavaScript evaluation check js_code = check_desc[3:].strip() return await self._run_js_check(page, js_code) else: # Text-based check return await self._run_text_check(page, check_desc) except Exception as e: logger.error(f"Error running check '{check_desc}': {e}") return { "check": check_desc, "passed": False, "score": 0.0, "reason": f"Error: {e}", } async def _run_js_check(self, page, js_code: str) -> dict[str, Any]: """Run JavaScript evaluation check. Args: page: Playwright page js_code: JavaScript code to evaluate Returns: Check result """ try: result = await page.evaluate(js_code) if isinstance(result, bool): return { "check": f"JS: {js_code[:50]}...", "passed": result, "score": 1.0 if result else 0.0, "reason": f"JavaScript evaluated to {result}", } else: return { "check": f"JS: {js_code[:50]}...", "passed": True, "score": 1.0, "reason": f"JavaScript returned: {result}", } except Exception as e: return { "check": f"JS: {js_code[:50]}...", "passed": False, "score": 0.0, "reason": f"JavaScript error: {e}", } async def _run_text_check(self, page, check_desc: str) -> dict[str, Any]: """Run text-based check. Args: page: Playwright page check_desc: Check description Returns: Check result """ # Extract selector and expected condition # Examples: # - "Page title equals 'Sales Summary xyz'" # - "Element #total-sales exists and displays numeric value" # - "Bootstrap 5 CSS loaded from jsdelivr" content = await page.content() # Check for page title if "page title" in check_desc.lower(): title = await page.title() match = re.search(r"['\"]([^'\"]+)['\"]", check_desc) if match: expected = match.group(1) passed = expected in title return { "check": check_desc, "passed": passed, "score": 1.0 if passed else 0.0, "reason": f"Title is '{title}', expected to contain '{expected}'", } # Check for element existence if "element" in check_desc.lower(): # Extract selector selector_match = re.search(r"(#[\w-]+|\.[a-z-]+|[a-z]+)", check_desc, re.IGNORECASE) if selector_match: selector = selector_match.group(1) try: element = await page.query_selector(selector) if element: text = await element.text_content() return { "check": check_desc, "passed": True, "score": 1.0, "reason": f"Element {selector} found with content: {text[:50]}...", } else: return { "check": check_desc, "passed": False, "score": 0.0, "reason": f"Element {selector} not found", } except Exception as e: return { "check": check_desc, "passed": False, "score": 0.0, "reason": f"Error finding element: {e}", } # Check for Bootstrap/library loading if "bootstrap" in check_desc.lower() or "cdn" in check_desc.lower(): has_bootstrap = "bootstrap" in content.lower() return { "check": check_desc, "passed": has_bootstrap, "score": 1.0 if has_bootstrap else 0.0, "reason": f"Bootstrap {'found' if has_bootstrap else 'not found'} in page", } # Default: check if description keywords appear in content keywords = re.findall(r"\b\w{4,}\b", check_desc.lower()) matches = sum(1 for kw in keywords if kw in content.lower()) score = min(matches / max(len(keywords), 1), 1.0) return { "check": check_desc, "passed": score >= 0.5, "score": score, "reason": f"Matched {matches}/{len(keywords)} keywords", }