thenuke02/cs2-analyzer
0
1"""2Full visual audit of OpenSight — uploads demo, screenshots every tab and sub-tab.3 4Usage:5 py -3.11 scripts/visual_audit.py # Upload demo + screenshot all6 py -3.11 scripts/visual_audit.py --job-id <ID> # Skip upload, reuse job7 py -3.11 scripts/visual_audit.py --job-id <ID> --tab overview # Single tab only8 py -3.11 scripts/visual_audit.py --job-id <ID> --batch 3 # Single batch only9"""10 11import argparse12import json13import os14import sys15import time16from pathlib import Path17 18from playwright.sync_api import sync_playwright19 20BASE_URL = "http://localhost:7860"21GOLDEN_RESULT = Path(__file__).parent.parent / "tests" / "fixtures" / "golden_result.json"22SCREENSHOT_DIR = Path(__file__).parent.parent / "screenshots"23 24# All main tabs (data-tab attribute values)25MAIN_TABS = [26 "overview",27 "match-details",28 "timeline",29 "matrix",30 "headtohead",31 "heatmap",32 "statsgraph",33 "economy",34 "replay",35 "coaching",36 "gameplan",37 "tactical",38 "stratsteal",39 "selfreview",40 "veto",41 "yourmatch",42 "trends",43 "profile",44]45 46# All Match Details sub-tabs (data-subtab attribute values)47MATCH_DETAIL_SUBTABS = [48 "general",49 "timeline",50 "aim",51 "utility",52 "activity",53 "trades",54 "spray-transfers",55 "opening-duels",56 "entry-breakdown",57 "mistakes",58 "lurk-stats",59 "positioning",60 "clutches",61 "synergy",62 "side-stats",63 "economy-detail",64 "events",65 "round-impact",66 "round-type",67 "by-half",68 "weapons",69 "map-control",70 "utility-timeline",71 "kill-sequence",72 "score-progression",73 "perf-heatmap",74 "team-radar",75]76 77 78def screenshot(page, name, full_page=True):79 """Take and save a screenshot."""80 path = SCREENSHOT_DIR / f"{name}.png"81 page.screenshot(path=str(path), full_page=full_page)82 print(f" [OK] {name}.png")83 84 85def click_tab(page, tab_id):86 """Click a main tab via JS (avoids visibility issues with dropdown groups)."""87 page.evaluate(88 f"""(() => {{89 const btn = document.querySelector('.tab-btn[data-tab="{tab_id}"]');90 if (!btn) return;91 const group = btn.closest('.tab-group');92 if (group && !group.classList.contains('open')) {{93 const groupBtn = group.querySelector('.tab-group-btn');94 if (groupBtn) groupBtn.click();95 }}96 setTimeout(() => btn.click(), 100);97 }})()"""98 )99 page.wait_for_timeout(1500)100 101 102def click_subtab(page, subtab_id):103 """Click a Match Details sub-tab via JS."""104 page.evaluate(105 f"""(() => {{106 const btn = document.querySelector('.sub-tab[data-subtab="{subtab_id}"]');107 if (btn) btn.click();108 }})()"""109 )110 page.wait_for_timeout(1000)111 112 113def inject_golden_result(page):114 """Load the analyze page and inject golden_result.json via renderResults()."""115 print("\n=== INJECTING GOLDEN RESULT ===")116 page.goto(f"{BASE_URL}/analyze", wait_until="networkidle")117 page.wait_for_timeout(1000)118 119 # Read golden result fixture120 with open(GOLDEN_RESULT) as f:121 data = json.load(f)122 123 # Inject via JS — call renderResults directly124 page.evaluate(125 """(data) => {126 window.currentAnalysis = data;127 window.__opensightResults = data;128 if (typeof renderResults === 'function') {129 renderResults(data);130 }131 }""",132 data,133 )134 page.wait_for_timeout(3000) # Let all lazy content settle135 136 has_results = page.evaluate(137 """138 document.querySelector('#results') &&139 document.querySelector('#results').children.length > 0140 """141 )142 if has_results:143 print(" Results injected and rendered")144 else:145 print(" [WARN] Results container may be empty — check screenshot")146 147 148def load_results(page, job_id):149 """Load analysis results using a known job_id."""150 print(f"\n=== LOADING RESULTS (job_id: {job_id}) ===")151 page.goto(f"{BASE_URL}/analyze?job={job_id}", wait_until="networkidle")152 153 start = time.time()154 while time.time() - start < 30:155 has_results = page.evaluate(156 """157 document.querySelector('#results') &&158 document.querySelector('#results').children.length > 0159 """160 )161 if has_results:162 break163 page.wait_for_timeout(1000)164 165 page.wait_for_timeout(2000)166 print(" Results loaded")167 168 169def batch1_core(page):170 """Batch 1: Core tabs — what every user sees first."""171 print("\n=== BATCH 1: CORE TABS ===")172 173 # Landing page174 p2 = page.context.new_page()175 p2.set_viewport_size({"width": 1440, "height": 900})176 p2.goto(f"{BASE_URL}/", wait_until="networkidle")177 p2.wait_for_timeout(1000)178 screenshot(p2, "b1_01_landing")179 p2.close()180 181 # Empty analyze page182 p3 = page.context.new_page()183 p3.set_viewport_size({"width": 1440, "height": 900})184 p3.goto(f"{BASE_URL}/analyze", wait_until="networkidle")185 p3.wait_for_timeout(1000)186 screenshot(p3, "b1_02_upload_empty")187 p3.close()188 189 190def batch1_results(page):191 """Batch 1 continued: Overview and Match Details general."""192 click_tab(page, "overview")193 page.evaluate("window.scrollTo(0, 0)")194 page.wait_for_timeout(500)195 screenshot(page, "b1_03_overview")196 197 click_tab(page, "match-details")198 click_subtab(page, "general")199 page.evaluate("window.scrollTo(0, 0)")200 page.wait_for_timeout(500)201 screenshot(page, "b1_04_match_details_general")202 203 204def batch2_match_data(page):205 """Batch 2: Match data tabs."""206 print("\n=== BATCH 2: MATCH DATA TABS ===")207 208 for tab_id in ["timeline", "matrix", "headtohead", "economy", "statsgraph"]:209 click_tab(page, tab_id)210 page.evaluate("window.scrollTo(0, 0)")211 page.wait_for_timeout(1000)212 screenshot(page, f"b2_{tab_id}")213 214 215def batch3_match_detail_subtabs(page):216 """Batch 3: All Match Details sub-tabs."""217 print("\n=== BATCH 3: ALL MATCH DETAILS SUB-TABS ===")218 219 click_tab(page, "match-details")220 page.wait_for_timeout(500)221 222 for i, subtab in enumerate(MATCH_DETAIL_SUBTABS):223 click_subtab(page, subtab)224 page.evaluate("window.scrollTo(0, 0)")225 page.wait_for_timeout(300)226 screenshot(page, f"b3_{i + 1:02d}_{subtab}")227 228 229def batch4_analysis_tools(page):230 """Batch 4: Analysis & AI tools tabs."""231 print("\n=== BATCH 4: ANALYSIS & AI TOOLS ===")232 233 # Heatmap — default234 click_tab(page, "heatmap")235 page.evaluate("window.scrollTo(0, 0)")236 page.wait_for_timeout(1500)237 screenshot(page, "b4_heatmap_default")238 239 # Heatmap — dots240 page.evaluate(241 """(() => {242 const btn = document.querySelector('[data-mode="dots"]');243 if (btn) btn.click();244 })()"""245 )246 page.wait_for_timeout(1000)247 screenshot(page, "b4_heatmap_dots")248 249 # Heatmap — zones250 page.evaluate(251 """(() => {252 const btn = document.querySelector('[data-mode="zones"]');253 if (btn) btn.click();254 })()"""255 )256 page.wait_for_timeout(1000)257 screenshot(page, "b4_heatmap_zones")258 259 # 2D Replay260 click_tab(page, "replay")261 page.evaluate("window.scrollTo(0, 0)")262 page.wait_for_timeout(2000)263 screenshot(page, "b4_replay")264 265 # AI tabs266 for tab_id in [267 "coaching",268 "gameplan",269 "tactical",270 "stratsteal",271 "selfreview",272 "veto",273 ]:274 click_tab(page, tab_id)275 page.evaluate("window.scrollTo(0, 0)")276 page.wait_for_timeout(500)277 screenshot(page, f"b4_{tab_id}")278 279 280def batch5_personal(page):281 """Batch 5: Personal tabs + standalone pages."""282 print("\n=== BATCH 5: PERSONAL TABS ===")283 284 for tab_id in ["yourmatch", "trends", "profile"]:285 click_tab(page, tab_id)286 page.evaluate("window.scrollTo(0, 0)")287 page.wait_for_timeout(500)288 screenshot(page, f"b5_{tab_id}")289 290 # Profile page standalone291 p2 = page.context.new_page()292 p2.set_viewport_size({"width": 1440, "height": 900})293 p2.goto(f"{BASE_URL}/profile", wait_until="networkidle")294 p2.wait_for_timeout(1000)295 screenshot(p2, "b5_profile_page")296 p2.close()297 298 # Compare page standalone299 p3 = page.context.new_page()300 p3.set_viewport_size({"width": 1440, "height": 900})301 p3.goto(f"{BASE_URL}/compare", wait_until="networkidle")302 p3.wait_for_timeout(1000)303 screenshot(p3, "b5_compare_page")304 p3.close()305 306 307def batch6_mobile(page):308 """Batch 6: Mobile (375px) views."""309 print("\n=== BATCH 6: MOBILE VIEWS ===")310 311 # Landing mobile312 p = page.context.new_page()313 p.set_viewport_size({"width": 375, "height": 812})314 p.goto(f"{BASE_URL}/", wait_until="networkidle")315 p.wait_for_timeout(1000)316 screenshot(p, "b6_mobile_landing")317 p.close()318 319 # Set main page to mobile320 page.set_viewport_size({"width": 375, "height": 812})321 page.wait_for_timeout(500)322 323 # Core tabs mobile324 for tab_id in [325 "overview",326 "match-details",327 "timeline",328 "matrix",329 "heatmap",330 "economy",331 "statsgraph",332 ]:333 click_tab(page, tab_id)334 page.evaluate("window.scrollTo(0, 0)")335 page.wait_for_timeout(800)336 screenshot(page, f"b6_mobile_{tab_id}")337 338 # Key sub-tabs mobile339 click_tab(page, "match-details")340 for subtab in [341 "general",342 "aim",343 "utility",344 "trades",345 "economy-detail",346 "side-stats",347 ]:348 click_subtab(page, subtab)349 page.evaluate("window.scrollTo(0, 0)")350 page.wait_for_timeout(500)351 screenshot(page, f"b6_mobile_subtab_{subtab}")352 353 # Restore desktop354 page.set_viewport_size({"width": 1440, "height": 900})355 page.wait_for_timeout(500)356 357 358def main():359 parser = argparse.ArgumentParser(description="Visual audit of OpenSight")360 parser.add_argument("--job-id", help="Reuse existing job ID (skip upload)")361 parser.add_argument("--tab", help="Screenshot only this tab")362 parser.add_argument("--batch", type=int, help="Run only this batch (1-6)")363 # Support positional job_id for backwards compat364 parser.add_argument("job_id_pos", nargs="?", help=argparse.SUPPRESS)365 args = parser.parse_args()366 367 job_id_arg = args.job_id or args.job_id_pos368 369 SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)370 371 # Clean old screenshots only if doing a full run372 if not args.tab and not args.batch:373 for f in SCREENSHOT_DIR.glob("*.png"):374 f.unlink()375 376 print("=" * 60)377 print("OpenSight Visual Audit")378 print(f"Server: {BASE_URL}")379 print(f"Fixture: {GOLDEN_RESULT}")380 print("=" * 60)381 382 with sync_playwright() as p:383 browser = p.chromium.launch(headless=True)384 context = browser.new_context(viewport={"width": 1440, "height": 900})385 page = context.new_page()386 387 # Load results — inject golden fixture or load from job_id388 job_id = job_id_arg389 if job_id:390 load_results(page, job_id)391 else:392 inject_golden_result(page)393 job_id = "golden-fixture"394 395 # Single tab mode396 if args.tab:397 if args.tab in MATCH_DETAIL_SUBTABS:398 click_tab(page, "match-details")399 click_subtab(page, args.tab)400 else:401 click_tab(page, args.tab)402 page.evaluate("window.scrollTo(0, 0)")403 page.wait_for_timeout(500)404 screenshot(page, f"single_{args.tab}")405 browser.close()406 print(f"\nDone. JOB_ID={job_id}")407 return408 409 # Batch mode or full run410 batches = {411 1: lambda: (batch1_core(page), batch1_results(page)),412 2: lambda: batch2_match_data(page),413 3: lambda: batch3_match_detail_subtabs(page),414 4: lambda: batch4_analysis_tools(page),415 5: lambda: batch5_personal(page),416 6: lambda: batch6_mobile(page),417 }418 419 if args.batch:420 if args.batch in batches:421 batches[args.batch]()422 else:423 print(f"[ERROR] Invalid batch {args.batch}. Valid: 1-6")424 else:425 batch1_core(page)426 batch1_results(page)427 batch2_match_data(page)428 batch3_match_detail_subtabs(page)429 batch4_analysis_tools(page)430 batch5_personal(page)431 batch6_mobile(page)432 433 browser.close()434 435 screenshots = list(SCREENSHOT_DIR.glob("*.png"))436 print(f"\n{'=' * 60}")437 print("AUDIT COMPLETE")438 print(f"{'=' * 60}")439 print(f"Screenshots: {len(screenshots)}")440 print(f"Saved to: {SCREENSHOT_DIR}")441 print(f"JOB_ID: {job_id}")442 print(f"\nReuse with: py -3.11 scripts/visual_audit.py --job-id {job_id}")443 444 445if __name__ == "__main__":446 main()447 