cursorflow 1.3.7__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,12 @@ from playwright.async_api import async_playwright, Page, Browser, BrowserContext
12
12
  import logging
13
13
  from pathlib import Path
14
14
 
15
+ from .trace_manager import TraceManager
16
+ # v2.0 Enhancement: Hot Reload Intelligence
17
+ from .hmr_detector import HMRDetector
18
+ # v2.0 Enhancement: Enhanced Error Context Collection
19
+ from .error_context_collector import ErrorContextCollector
20
+
15
21
 
16
22
  class BrowserController:
17
23
  """
@@ -48,11 +54,29 @@ class BrowserController:
48
54
  self.logger = logging.getLogger(__name__)
49
55
 
50
56
  # Ensure artifacts directory exists
57
+ artifacts_base = Path(".cursorflow/artifacts")
58
+ artifacts_base.mkdir(parents=True, exist_ok=True)
51
59
  Path(".cursorflow/artifacts/screenshots").mkdir(parents=True, exist_ok=True)
52
60
 
53
- async def initialize(self):
54
- """Initialize browser with universal settings"""
61
+ # Initialize trace manager for v2.0 trace recording
62
+ self.trace_manager = TraceManager(artifacts_base)
63
+ self.session_id = None
64
+
65
+ # v2.0 Enhancement: Hot Reload Intelligence
66
+ self.hmr_detector = HMRDetector(base_url)
67
+ self.hmr_monitoring_active = False
68
+
69
+ # v2.0 Enhancement: Enhanced Error Context Collection
70
+ self.error_context_collector = None # Will be initialized after page is ready
71
+
72
+ async def initialize(self, session_id: Optional[str] = None):
73
+ """Initialize browser with universal settings and start trace recording"""
55
74
  try:
75
+ # Generate session ID if not provided
76
+ if session_id is None:
77
+ session_id = f"session_{int(time.time())}"
78
+ self.session_id = session_id
79
+
56
80
  self.playwright = await async_playwright().start()
57
81
 
58
82
  # Browser configuration - works for any framework
@@ -81,6 +105,22 @@ class BrowserController:
81
105
  self.context = await self.browser.new_context(**context_config)
82
106
  self.page = await self.context.new_page()
83
107
 
108
+ # v2.0 Enhancement: Initialize Error Context Collector
109
+ self.error_context_collector = ErrorContextCollector(self.page, self.logger)
110
+
111
+ # Set references to browser data for context collection
112
+ self.error_context_collector.set_browser_data_references(
113
+ self.console_logs,
114
+ self.network_requests
115
+ )
116
+
117
+ # Start trace recording for comprehensive debugging (v2.0 feature)
118
+ try:
119
+ trace_path = await self.trace_manager.start_trace(self.context, self.session_id)
120
+ self.logger.info(f"📹 Trace recording started: {trace_path}")
121
+ except Exception as trace_error:
122
+ self.logger.warning(f"Trace recording failed to start: {trace_error}")
123
+
84
124
  # Set up universal event listeners
85
125
  await self._setup_event_listeners()
86
126
 
@@ -91,6 +131,16 @@ class BrowserController:
91
131
 
92
132
  except Exception as e:
93
133
  self.logger.error(f"Browser initialization failed: {e}")
134
+
135
+ # Save error trace if context was created (v2.0 feature)
136
+ if self.context and self.trace_manager:
137
+ try:
138
+ error_trace = await self.trace_manager.stop_trace_on_error(self.context, e)
139
+ if error_trace:
140
+ self.logger.error(f"📹 Error trace saved: {error_trace}")
141
+ except Exception:
142
+ pass # Don't let trace errors mask the original error
143
+
94
144
  raise
95
145
 
96
146
  async def _setup_event_listeners(self):
@@ -108,7 +158,7 @@ class BrowserController:
108
158
  self.page.on("crash", self._handle_page_crash)
109
159
 
110
160
  def _handle_console_message(self, msg):
111
- """Handle console messages from any framework"""
161
+ """Handle console messages from any framework with enhanced error context collection"""
112
162
  log_entry = {
113
163
  "timestamp": time.time(),
114
164
  "type": msg.type,
@@ -126,11 +176,33 @@ class BrowserController:
126
176
  # Enhanced logging for better correlation
127
177
  if msg.type == "error":
128
178
  self.logger.error(f"Console Error: {msg.text} at {msg.location}")
179
+
180
+ # v2.0 Enhancement: Trigger error context collection for console errors
181
+ if self.error_context_collector:
182
+ error_event = {
183
+ 'type': 'console_error',
184
+ 'message': msg.text,
185
+ 'location': log_entry['location'],
186
+ 'stack_trace': log_entry['stack_trace'],
187
+ 'timestamp': log_entry['timestamp']
188
+ }
189
+ # Capture context asynchronously (don't block the event handler)
190
+ asyncio.create_task(self._collect_error_context_async(error_event))
191
+
129
192
  elif msg.type == "warning":
130
193
  self.logger.warning(f"Console Warning: {msg.text}")
131
194
  elif msg.type in ["log", "info"] and any(keyword in msg.text.lower() for keyword in ["error", "failed", "exception"]):
132
195
  # Catch application logs that indicate errors
133
196
  self.logger.warning(f"App Error Log: {msg.text}")
197
+
198
+ # v2.0 Enhancement: Collect context for application error logs too
199
+ if self.error_context_collector:
200
+ error_event = {
201
+ 'type': 'app_error_log',
202
+ 'message': msg.text,
203
+ 'timestamp': log_entry['timestamp']
204
+ }
205
+ asyncio.create_task(self._collect_error_context_async(error_event))
134
206
 
135
207
  def _handle_request(self, request):
136
208
  """Handle network requests - framework agnostic"""
@@ -190,7 +262,7 @@ class BrowserController:
190
262
  self.logger.debug(f"Request payload: {request.post_data}")
191
263
 
192
264
  def _handle_response(self, response):
193
- """Handle network responses - framework agnostic"""
265
+ """Handle network responses with enhanced error context collection"""
194
266
  response_data = {
195
267
  "timestamp": time.time(),
196
268
  "type": "response",
@@ -206,6 +278,19 @@ class BrowserController:
206
278
  # Log failed requests for correlation
207
279
  if response.status >= 400:
208
280
  self.logger.warning(f"Failed Response: {response.status} {response.url}")
281
+
282
+ # v2.0 Enhancement: Trigger error context collection for failed requests
283
+ if self.error_context_collector:
284
+ error_event = {
285
+ 'type': 'network_error',
286
+ 'url': response.url,
287
+ 'status': response.status,
288
+ 'status_text': response.status_text,
289
+ 'headers': dict(response.headers),
290
+ 'timestamp': response_data['timestamp']
291
+ }
292
+ # Capture context asynchronously
293
+ asyncio.create_task(self._collect_error_context_async(error_event))
209
294
 
210
295
  # Capture response body for important requests
211
296
  should_capture_body = (
@@ -330,7 +415,9 @@ class BrowserController:
330
415
  "screenshot_path": screenshot_filename,
331
416
  "timestamp": timestamp,
332
417
  "name": name,
333
- "full_page": full_page
418
+ "full_page": full_page,
419
+ "session_id": self.session_id,
420
+ "trace_info": self.trace_manager.get_trace_info() if self.trace_manager else None
334
421
  }
335
422
 
336
423
  if capture_comprehensive_data:
@@ -449,8 +536,20 @@ class BrowserController:
449
536
  return {}
450
537
 
451
538
  async def cleanup(self):
452
- """Clean up browser resources"""
539
+ """Clean up browser resources and stop trace recording"""
453
540
  try:
541
+ # Stop trace recording first (v2.0 feature)
542
+ trace_path = None
543
+ if self.context and self.trace_manager:
544
+ try:
545
+ trace_path = await self.trace_manager.stop_trace(self.context)
546
+ if trace_path:
547
+ self.logger.info(f"📹 Trace recording saved: {trace_path}")
548
+ self.logger.info(f"View trace: {self.trace_manager.get_viewing_instructions(trace_path)}")
549
+ except Exception as trace_error:
550
+ self.logger.warning(f"Trace recording cleanup failed: {trace_error}")
551
+
552
+ # Clean up browser resources
454
553
  if self.page:
455
554
  await self.page.close()
456
555
  if self.context:
@@ -461,9 +560,11 @@ class BrowserController:
461
560
  await self.playwright.stop()
462
561
 
463
562
  self.logger.info("Browser cleanup completed")
563
+ return trace_path
464
564
 
465
565
  except Exception as e:
466
566
  self.logger.error(f"Browser cleanup failed: {e}")
567
+ return None
467
568
 
468
569
  async def _capture_response_body(self, response):
469
570
  """Capture response body for API calls and errors"""
@@ -499,7 +600,72 @@ class BrowserController:
499
600
  break
500
601
 
501
602
  except Exception as e:
502
- self.logger.warning(f"Failed to capture response body: {e}")
603
+ self.logger.error(f"Response body capture failed for {response.url}: {e}")
604
+ # Add error info to the response record
605
+ for req in reversed(self.network_requests):
606
+ if (req.get("type") == "response" and req.get("url") == response.url):
607
+ req["body_capture_error"] = str(e)
608
+ break
609
+
610
+ def _categorize_http_error(self, status_code: int) -> str:
611
+ """Categorize HTTP errors for better debugging (v2.0 enhancement)"""
612
+ if 400 <= status_code < 500:
613
+ error_categories = {
614
+ 400: "Bad Request",
615
+ 401: "Authentication Required",
616
+ 403: "Access Forbidden",
617
+ 404: "Resource Not Found",
618
+ 405: "Method Not Allowed",
619
+ 409: "Conflict",
620
+ 422: "Validation Error",
621
+ 429: "Rate Limited"
622
+ }
623
+ return error_categories.get(status_code, "Client Error")
624
+ elif 500 <= status_code < 600:
625
+ error_categories = {
626
+ 500: "Server Error",
627
+ 502: "Bad Gateway",
628
+ 503: "Service Unavailable",
629
+ 504: "Gateway Timeout"
630
+ }
631
+ return error_categories.get(status_code, "Server Error")
632
+ else:
633
+ return "Unknown Error"
634
+
635
+ def _analyze_error_cause(self, status_code: int, body: str) -> str:
636
+ """Analyze likely cause of HTTP errors (v2.0 enhancement)"""
637
+ body_lower = body.lower()
638
+
639
+ if status_code == 401:
640
+ if "token" in body_lower or "jwt" in body_lower:
641
+ return "Invalid or expired authentication token"
642
+ elif "login" in body_lower or "credential" in body_lower:
643
+ return "Invalid credentials"
644
+ else:
645
+ return "Authentication required"
646
+ elif status_code == 403:
647
+ if "permission" in body_lower or "role" in body_lower:
648
+ return "Insufficient permissions"
649
+ else:
650
+ return "Access denied"
651
+ elif status_code == 404:
652
+ return "Endpoint or resource does not exist"
653
+ elif status_code == 422:
654
+ if "validation" in body_lower:
655
+ return "Input validation failed"
656
+ else:
657
+ return "Request data invalid"
658
+ elif status_code == 429:
659
+ return "Too many requests - rate limit exceeded"
660
+ elif status_code >= 500:
661
+ if "database" in body_lower or "sql" in body_lower:
662
+ return "Database connection or query error"
663
+ elif "timeout" in body_lower:
664
+ return "Server timeout"
665
+ else:
666
+ return "Internal server error"
667
+ else:
668
+ return "Unknown error condition"
503
669
 
504
670
  async def capture_network_har(self) -> Dict:
505
671
  """Capture full network activity as HAR format"""
@@ -551,7 +717,7 @@ class BrowserController:
551
717
  return failed
552
718
 
553
719
  async def _capture_comprehensive_page_analysis(self) -> Dict[str, Any]:
554
- """Capture comprehensive page analysis including DOM, network, console, and performance data"""
720
+ """Enhanced comprehensive page analysis with v2.0 features: fonts, animations, resources, storage"""
555
721
  try:
556
722
  # Capture DOM analysis
557
723
  dom_analysis = await self._capture_dom_analysis()
@@ -568,8 +734,23 @@ class BrowserController:
568
734
  # Capture page state information
569
735
  page_state = await self._capture_page_state()
570
736
 
571
- # Create analysis summary
572
- analysis_summary = self._create_analysis_summary(dom_analysis, network_data, console_data, performance_data)
737
+ # v2.0 Enhancement: Font loading status
738
+ font_analysis = await self._capture_font_loading_status()
739
+
740
+ # v2.0 Enhancement: Animation state
741
+ animation_analysis = await self._capture_animation_state()
742
+
743
+ # v2.0 Enhancement: Resource loading analysis
744
+ resource_analysis = await self._capture_resource_loading_analysis()
745
+
746
+ # v2.0 Enhancement: Storage state (read-only)
747
+ storage_analysis = await self._capture_storage_state()
748
+
749
+ # Create enhanced analysis summary
750
+ analysis_summary = self._create_analysis_summary(
751
+ dom_analysis, network_data, console_data, performance_data,
752
+ font_analysis, animation_analysis, resource_analysis, storage_analysis
753
+ )
573
754
 
574
755
  return {
575
756
  "dom_analysis": dom_analysis,
@@ -577,37 +758,734 @@ class BrowserController:
577
758
  "console_data": console_data,
578
759
  "performance_data": performance_data,
579
760
  "page_state": page_state,
761
+
762
+ # v2.0 Enhancements
763
+ "font_analysis": font_analysis,
764
+ "animation_analysis": animation_analysis,
765
+ "resource_analysis": resource_analysis,
766
+ "storage_analysis": storage_analysis,
767
+
580
768
  "analysis_summary": analysis_summary,
581
769
  "capture_timestamp": time.time(),
582
770
  "analysis_version": "2.0"
583
771
  }
584
772
 
585
773
  except Exception as e:
586
- self.logger.error(f"Comprehensive page analysis failed: {e}")
774
+ self.logger.error(f"Enhanced comprehensive page analysis failed: {e}")
587
775
  return {"error": str(e), "capture_timestamp": time.time()}
588
776
 
777
+ async def _capture_font_loading_status(self) -> Dict[str, Any]:
778
+ """v2.0 Enhancement: Capture comprehensive font loading analysis"""
779
+ try:
780
+ font_analysis = await self.page.evaluate("""
781
+ async () => {
782
+ // Wait for document.fonts to be available
783
+ if (!document.fonts) {
784
+ return {
785
+ fonts_supported: false,
786
+ error: "Font Loading API not supported"
787
+ };
788
+ }
789
+
790
+ // Get all font faces
791
+ const fontFaces = Array.from(document.fonts);
792
+
793
+ // Analyze font loading status
794
+ const fontStatus = {
795
+ total_fonts: fontFaces.length,
796
+ loaded_fonts: 0,
797
+ loading_fonts: 0,
798
+ failed_fonts: 0,
799
+ unloaded_fonts: 0,
800
+ font_details: []
801
+ };
802
+
803
+ // Check if fonts are ready
804
+ const fontsReady = await document.fonts.ready;
805
+
806
+ // Analyze each font face
807
+ fontFaces.forEach(fontFace => {
808
+ const fontInfo = {
809
+ family: fontFace.family,
810
+ style: fontFace.style,
811
+ weight: fontFace.weight,
812
+ stretch: fontFace.stretch,
813
+ unicode_range: fontFace.unicodeRange,
814
+ variant: fontFace.variant,
815
+ status: fontFace.status,
816
+ source: fontFace.src || 'system'
817
+ };
818
+
819
+ // Count by status
820
+ switch (fontFace.status) {
821
+ case 'loaded':
822
+ fontStatus.loaded_fonts++;
823
+ break;
824
+ case 'loading':
825
+ fontStatus.loading_fonts++;
826
+ break;
827
+ case 'error':
828
+ fontStatus.failed_fonts++;
829
+ break;
830
+ case 'unloaded':
831
+ fontStatus.unloaded_fonts++;
832
+ break;
833
+ }
834
+
835
+ fontStatus.font_details.push(fontInfo);
836
+ });
837
+
838
+ // Get computed font families used on the page
839
+ const usedFonts = new Set();
840
+ const elements = document.querySelectorAll('*');
841
+
842
+ elements.forEach(element => {
843
+ const computedStyle = window.getComputedStyle(element);
844
+ const fontFamily = computedStyle.fontFamily;
845
+ if (fontFamily && fontFamily !== 'inherit') {
846
+ usedFonts.add(fontFamily);
847
+ }
848
+ });
849
+
850
+ // Font loading performance
851
+ const fontLoadingMetrics = {
852
+ fonts_ready: fontsReady !== null,
853
+ loading_complete: fontStatus.loading_fonts === 0,
854
+ has_failures: fontStatus.failed_fonts > 0,
855
+ load_success_rate: fontStatus.total_fonts > 0 ?
856
+ (fontStatus.loaded_fonts / fontStatus.total_fonts * 100).toFixed(2) + '%' : '100%'
857
+ };
858
+
859
+ return {
860
+ fonts_supported: true,
861
+ font_status: fontStatus,
862
+ used_font_families: Array.from(usedFonts),
863
+ loading_metrics: fontLoadingMetrics,
864
+ capture_timestamp: Date.now()
865
+ };
866
+ }
867
+ """)
868
+
869
+ return font_analysis
870
+
871
+ except Exception as e:
872
+ self.logger.error(f"Font loading analysis failed: {e}")
873
+ return {
874
+ "fonts_supported": False,
875
+ "error": str(e),
876
+ "capture_timestamp": time.time()
877
+ }
878
+
879
+ async def _capture_animation_state(self) -> Dict[str, Any]:
880
+ """v2.0 Enhancement: Capture CSS animation and transition state"""
881
+ try:
882
+ animation_analysis = await self.page.evaluate("""
883
+ () => {
884
+ const animationData = {
885
+ total_animated_elements: 0,
886
+ running_animations: 0,
887
+ paused_animations: 0,
888
+ finished_animations: 0,
889
+ running_transitions: 0,
890
+ animation_details: [],
891
+ transition_details: []
892
+ };
893
+
894
+ // Get all elements on the page
895
+ const elements = document.querySelectorAll('*');
896
+
897
+ elements.forEach(element => {
898
+ // Check for CSS animations
899
+ const animations = element.getAnimations ? element.getAnimations() : [];
900
+
901
+ if (animations.length > 0) {
902
+ animationData.total_animated_elements++;
903
+
904
+ animations.forEach(animation => {
905
+ const animInfo = {
906
+ element_selector: element.tagName.toLowerCase() +
907
+ (element.id ? '#' + element.id : '') +
908
+ (element.className ? '.' + element.className.split(' ').join('.') : ''),
909
+ animation_name: animation.animationName || 'transition',
910
+ duration: animation.effect?.getTiming?.()?.duration || 0,
911
+ delay: animation.effect?.getTiming?.()?.delay || 0,
912
+ iterations: animation.effect?.getTiming?.()?.iterations || 1,
913
+ direction: animation.effect?.getTiming?.()?.direction || 'normal',
914
+ fill_mode: animation.effect?.getTiming?.()?.fill || 'none',
915
+ play_state: animation.playState,
916
+ current_time: animation.currentTime,
917
+ start_time: animation.startTime,
918
+ timeline: animation.timeline?.currentTime || null
919
+ };
920
+
921
+ // Count by play state
922
+ switch (animation.playState) {
923
+ case 'running':
924
+ if (animation.animationName) {
925
+ animationData.running_animations++;
926
+ } else {
927
+ animationData.running_transitions++;
928
+ }
929
+ break;
930
+ case 'paused':
931
+ animationData.paused_animations++;
932
+ break;
933
+ case 'finished':
934
+ animationData.finished_animations++;
935
+ break;
936
+ }
937
+
938
+ if (animation.animationName) {
939
+ animationData.animation_details.push(animInfo);
940
+ } else {
941
+ animationData.transition_details.push(animInfo);
942
+ }
943
+ });
944
+ }
945
+
946
+ // Also check computed styles for animation properties
947
+ const computedStyle = window.getComputedStyle(element);
948
+ const animationName = computedStyle.animationName;
949
+ const transitionProperty = computedStyle.transitionProperty;
950
+
951
+ if (animationName && animationName !== 'none' && animations.length === 0) {
952
+ // Animation defined but not running (possibly finished or not started)
953
+ animationData.animation_details.push({
954
+ element_selector: element.tagName.toLowerCase() +
955
+ (element.id ? '#' + element.id : '') +
956
+ (element.className ? '.' + element.className.split(' ').join('.') : ''),
957
+ animation_name: animationName,
958
+ duration: computedStyle.animationDuration,
959
+ delay: computedStyle.animationDelay,
960
+ iterations: computedStyle.animationIterationCount,
961
+ direction: computedStyle.animationDirection,
962
+ fill_mode: computedStyle.animationFillMode,
963
+ play_state: 'inactive',
964
+ timing_function: computedStyle.animationTimingFunction
965
+ });
966
+ }
967
+
968
+ if (transitionProperty && transitionProperty !== 'none' &&
969
+ !animationData.transition_details.some(t => t.element_selector.includes(element.tagName))) {
970
+ animationData.transition_details.push({
971
+ element_selector: element.tagName.toLowerCase() +
972
+ (element.id ? '#' + element.id : '') +
973
+ (element.className ? '.' + element.className.split(' ').join('.') : ''),
974
+ transition_property: transitionProperty,
975
+ duration: computedStyle.transitionDuration,
976
+ delay: computedStyle.transitionDelay,
977
+ timing_function: computedStyle.transitionTimingFunction,
978
+ play_state: 'ready'
979
+ });
980
+ }
981
+ });
982
+
983
+ // Animation performance summary
984
+ const animationSummary = {
985
+ has_active_animations: animationData.running_animations > 0 || animationData.running_transitions > 0,
986
+ performance_impact: animationData.running_animations > 10 ? 'high' :
987
+ animationData.running_animations > 3 ? 'medium' : 'low',
988
+ animation_stability: animationData.paused_animations === 0 && animationData.running_animations > 0 ? 'stable' : 'mixed',
989
+ total_active: animationData.running_animations + animationData.running_transitions
990
+ };
991
+
992
+ return {
993
+ ...animationData,
994
+ animation_summary: animationSummary,
995
+ capture_timestamp: Date.now()
996
+ };
997
+ }
998
+ """)
999
+
1000
+ return animation_analysis
1001
+
1002
+ except Exception as e:
1003
+ self.logger.error(f"Animation state analysis failed: {e}")
1004
+ return {
1005
+ "error": str(e),
1006
+ "total_animated_elements": 0,
1007
+ "capture_timestamp": time.time()
1008
+ }
1009
+
1010
+ async def _capture_resource_loading_analysis(self) -> Dict[str, Any]:
1011
+ """v2.0 Enhancement: Comprehensive resource loading analysis"""
1012
+ try:
1013
+ resource_analysis = await self.page.evaluate("""
1014
+ () => {
1015
+ // Get performance entries for all resources
1016
+ const resourceEntries = performance.getEntriesByType('resource');
1017
+ const navigationEntry = performance.getEntriesByType('navigation')[0];
1018
+
1019
+ const resourceData = {
1020
+ total_resources: resourceEntries.length,
1021
+ resource_types: {},
1022
+ loading_performance: {
1023
+ fastest_resource: null,
1024
+ slowest_resource: null,
1025
+ average_load_time: 0,
1026
+ total_transfer_size: 0,
1027
+ total_encoded_size: 0
1028
+ },
1029
+ resource_details: [],
1030
+ critical_resources: [],
1031
+ failed_resources: []
1032
+ };
1033
+
1034
+ let totalLoadTime = 0;
1035
+ let fastestTime = Infinity;
1036
+ let slowestTime = 0;
1037
+
1038
+ // Analyze each resource
1039
+ resourceEntries.forEach(entry => {
1040
+ const resourceType = entry.initiatorType || 'other';
1041
+ const loadTime = entry.responseEnd - entry.startTime;
1042
+
1043
+ // Count by type
1044
+ resourceData.resource_types[resourceType] = (resourceData.resource_types[resourceType] || 0) + 1;
1045
+
1046
+ // Performance tracking
1047
+ totalLoadTime += loadTime;
1048
+ if (loadTime < fastestTime) {
1049
+ fastestTime = loadTime;
1050
+ resourceData.loading_performance.fastest_resource = {
1051
+ name: entry.name,
1052
+ type: resourceType,
1053
+ load_time: loadTime
1054
+ };
1055
+ }
1056
+ if (loadTime > slowestTime) {
1057
+ slowestTime = loadTime;
1058
+ resourceData.loading_performance.slowest_resource = {
1059
+ name: entry.name,
1060
+ type: resourceType,
1061
+ load_time: loadTime
1062
+ };
1063
+ }
1064
+
1065
+ // Size tracking
1066
+ resourceData.loading_performance.total_transfer_size += entry.transferSize || 0;
1067
+ resourceData.loading_performance.total_encoded_size += entry.encodedBodySize || 0;
1068
+
1069
+ // Detailed resource info
1070
+ const resourceInfo = {
1071
+ name: entry.name,
1072
+ type: resourceType,
1073
+ start_time: entry.startTime,
1074
+ duration: loadTime,
1075
+ transfer_size: entry.transferSize || 0,
1076
+ encoded_size: entry.encodedBodySize || 0,
1077
+ decoded_size: entry.decodedBodySize || 0,
1078
+ dns_time: entry.domainLookupEnd - entry.domainLookupStart,
1079
+ connect_time: entry.connectEnd - entry.connectStart,
1080
+ request_time: entry.responseStart - entry.requestStart,
1081
+ response_time: entry.responseEnd - entry.responseStart,
1082
+ is_cached: entry.transferSize === 0 && entry.decodedBodySize > 0,
1083
+ protocol: entry.nextHopProtocol || 'unknown'
1084
+ };
1085
+
1086
+ resourceData.resource_details.push(resourceInfo);
1087
+
1088
+ // Identify critical resources (CSS, fonts, scripts in head)
1089
+ if (['stylesheet', 'script', 'font'].includes(resourceType) ||
1090
+ entry.name.includes('.css') || entry.name.includes('.js') ||
1091
+ entry.name.match(/\\.(woff|woff2|ttf|otf)$/)) {
1092
+ resourceData.critical_resources.push(resourceInfo);
1093
+ }
1094
+
1095
+ // Check for failed resources (this would need to be cross-referenced with network errors)
1096
+ if (loadTime === 0 || entry.transferSize === 0 && entry.decodedBodySize === 0) {
1097
+ resourceData.failed_resources.push(resourceInfo);
1098
+ }
1099
+ });
1100
+
1101
+ // Calculate averages
1102
+ resourceData.loading_performance.average_load_time =
1103
+ resourceEntries.length > 0 ? totalLoadTime / resourceEntries.length : 0;
1104
+
1105
+ // Resource loading summary
1106
+ const loadingSummary = {
1107
+ page_load_complete: navigationEntry ? navigationEntry.loadEventEnd > 0 : false,
1108
+ dom_content_loaded: navigationEntry ? navigationEntry.domContentLoadedEventEnd > 0 : false,
1109
+ critical_path_loaded: resourceData.critical_resources.every(r => r.duration > 0),
1110
+ has_failed_resources: resourceData.failed_resources.length > 0,
1111
+ resource_efficiency: {
1112
+ cached_resources: resourceData.resource_details.filter(r => r.is_cached).length,
1113
+ cache_hit_rate: resourceData.resource_details.length > 0 ?
1114
+ (resourceData.resource_details.filter(r => r.is_cached).length / resourceData.resource_details.length * 100).toFixed(2) + '%' : '0%',
1115
+ compression_ratio: resourceData.loading_performance.total_encoded_size > 0 ?
1116
+ (resourceData.loading_performance.total_transfer_size / resourceData.loading_performance.total_encoded_size).toFixed(2) : 'N/A'
1117
+ }
1118
+ };
1119
+
1120
+ return {
1121
+ ...resourceData,
1122
+ loading_summary: loadingSummary,
1123
+ capture_timestamp: Date.now()
1124
+ };
1125
+ }
1126
+ """)
1127
+
1128
+ return resource_analysis
1129
+
1130
+ except Exception as e:
1131
+ self.logger.error(f"Resource loading analysis failed: {e}")
1132
+ return {
1133
+ "error": str(e),
1134
+ "total_resources": 0,
1135
+ "capture_timestamp": time.time()
1136
+ }
1137
+
1138
+ async def _capture_storage_state(self) -> Dict[str, Any]:
1139
+ """v2.0 Enhancement: Capture browser storage state (read-only observation)"""
1140
+ try:
1141
+ storage_analysis = await self.page.evaluate("""
1142
+ () => {
1143
+ const storageData = {
1144
+ local_storage: {
1145
+ available: typeof localStorage !== 'undefined',
1146
+ item_count: 0,
1147
+ total_size_estimate: 0,
1148
+ keys: []
1149
+ },
1150
+ session_storage: {
1151
+ available: typeof sessionStorage !== 'undefined',
1152
+ item_count: 0,
1153
+ total_size_estimate: 0,
1154
+ keys: []
1155
+ },
1156
+ cookies: {
1157
+ available: typeof document.cookie !== 'undefined',
1158
+ cookie_count: 0,
1159
+ total_size_estimate: 0,
1160
+ cookie_names: []
1161
+ },
1162
+ indexed_db: {
1163
+ available: typeof indexedDB !== 'undefined',
1164
+ databases: []
1165
+ }
1166
+ };
1167
+
1168
+ // Analyze localStorage
1169
+ if (storageData.local_storage.available) {
1170
+ try {
1171
+ storageData.local_storage.item_count = localStorage.length;
1172
+ for (let i = 0; i < localStorage.length; i++) {
1173
+ const key = localStorage.key(i);
1174
+ const value = localStorage.getItem(key);
1175
+ storageData.local_storage.keys.push({
1176
+ key: key,
1177
+ size_estimate: (key.length + (value ? value.length : 0)) * 2 // rough UTF-16 estimate
1178
+ });
1179
+ storageData.local_storage.total_size_estimate += (key.length + (value ? value.length : 0)) * 2;
1180
+ }
1181
+ } catch (e) {
1182
+ storageData.local_storage.error = e.message;
1183
+ }
1184
+ }
1185
+
1186
+ // Analyze sessionStorage
1187
+ if (storageData.session_storage.available) {
1188
+ try {
1189
+ storageData.session_storage.item_count = sessionStorage.length;
1190
+ for (let i = 0; i < sessionStorage.length; i++) {
1191
+ const key = sessionStorage.key(i);
1192
+ const value = sessionStorage.getItem(key);
1193
+ storageData.session_storage.keys.push({
1194
+ key: key,
1195
+ size_estimate: (key.length + (value ? value.length : 0)) * 2
1196
+ });
1197
+ storageData.session_storage.total_size_estimate += (key.length + (value ? value.length : 0)) * 2;
1198
+ }
1199
+ } catch (e) {
1200
+ storageData.session_storage.error = e.message;
1201
+ }
1202
+ }
1203
+
1204
+ // Analyze cookies
1205
+ if (storageData.cookies.available) {
1206
+ try {
1207
+ const cookieString = document.cookie;
1208
+ if (cookieString) {
1209
+ const cookies = cookieString.split(';');
1210
+ storageData.cookies.cookie_count = cookies.length;
1211
+ storageData.cookies.total_size_estimate = cookieString.length;
1212
+
1213
+ cookies.forEach(cookie => {
1214
+ const [name] = cookie.trim().split('=');
1215
+ if (name) {
1216
+ storageData.cookies.cookie_names.push(name);
1217
+ }
1218
+ });
1219
+ }
1220
+ } catch (e) {
1221
+ storageData.cookies.error = e.message;
1222
+ }
1223
+ }
1224
+
1225
+ // IndexedDB analysis (basic availability check)
1226
+ if (storageData.indexed_db.available) {
1227
+ try {
1228
+ // Note: Full IndexedDB analysis would require async operations
1229
+ // For now, we just check availability
1230
+ storageData.indexed_db.status = 'available_but_not_analyzed';
1231
+ storageData.indexed_db.note = 'Full analysis requires async operations';
1232
+ } catch (e) {
1233
+ storageData.indexed_db.error = e.message;
1234
+ }
1235
+ }
1236
+
1237
+ // Storage summary
1238
+ const storageSummary = {
1239
+ has_stored_data: storageData.local_storage.item_count > 0 ||
1240
+ storageData.session_storage.item_count > 0 ||
1241
+ storageData.cookies.cookie_count > 0,
1242
+ total_estimated_size: storageData.local_storage.total_size_estimate +
1243
+ storageData.session_storage.total_size_estimate +
1244
+ storageData.cookies.total_size_estimate,
1245
+ storage_types_used: [
1246
+ storageData.local_storage.item_count > 0 ? 'localStorage' : null,
1247
+ storageData.session_storage.item_count > 0 ? 'sessionStorage' : null,
1248
+ storageData.cookies.cookie_count > 0 ? 'cookies' : null,
1249
+ storageData.indexed_db.available ? 'indexedDB' : null
1250
+ ].filter(Boolean),
1251
+ privacy_indicators: {
1252
+ has_tracking_cookies: storageData.cookies.cookie_names.some(name =>
1253
+ ['_ga', '_gid', '_fbp', '_gat', 'utm_'].some(tracker => name.includes(tracker))),
1254
+ has_session_data: storageData.session_storage.item_count > 0,
1255
+ has_persistent_data: storageData.local_storage.item_count > 0
1256
+ }
1257
+ };
1258
+
1259
+ return {
1260
+ ...storageData,
1261
+ storage_summary: storageSummary,
1262
+ capture_timestamp: Date.now()
1263
+ };
1264
+ }
1265
+ """)
1266
+
1267
+ return storage_analysis
1268
+
1269
+ except Exception as e:
1270
+ self.logger.error(f"Storage state analysis failed: {e}")
1271
+ return {
1272
+ "error": str(e),
1273
+ "capture_timestamp": time.time()
1274
+ }
1275
+
589
1276
  async def _capture_dom_analysis(self) -> Dict[str, Any]:
590
- """Capture comprehensive DOM structure and CSS data for every screenshot"""
1277
+ """Enhanced DOM analysis with multi-selector strategies and accessibility data (v2.0)"""
591
1278
  try:
592
1279
  dom_analysis = await self.page.evaluate("""
593
1280
  () => {
594
- // Helper function to get element path
595
- function getElementPath(element) {
596
- const path = [];
597
- while (element && element.nodeType === Node.ELEMENT_NODE) {
598
- let selector = element.nodeName.toLowerCase();
1281
+ // Enhanced helper function to generate multiple selector strategies
1282
+ function generateMultipleSelectors(element) {
1283
+ const selectors = {};
1284
+
1285
+ // CSS selector (improved)
1286
+ let cssSelector = element.tagName.toLowerCase();
1287
+ if (element.id) {
1288
+ cssSelector = '#' + element.id;
1289
+ selectors.css = cssSelector;
1290
+ } else if (element.className && typeof element.className === 'string') {
1291
+ const classes = element.className.split(' ').filter(c => c.trim());
1292
+ if (classes.length > 0) {
1293
+ cssSelector += '.' + classes.join('.');
1294
+ selectors.css = cssSelector;
1295
+ }
1296
+ } else {
1297
+ selectors.css = cssSelector;
1298
+ }
1299
+
1300
+ // XPath selector
1301
+ function getXPath(element) {
599
1302
  if (element.id) {
600
- selector += '#' + element.id;
601
- } else if (element.className && typeof element.className === 'string') {
602
- selector += '.' + element.className.split(' ').filter(c => c).join('.');
1303
+ return `//*[@id="${element.id}"]`;
1304
+ }
1305
+
1306
+ let path = '';
1307
+ let current = element;
1308
+
1309
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
1310
+ let index = 0;
1311
+ let sibling = current.previousSibling;
1312
+
1313
+ while (sibling) {
1314
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
1315
+ index++;
1316
+ }
1317
+ sibling = sibling.previousSibling;
1318
+ }
1319
+
1320
+ const tagName = current.tagName.toLowerCase();
1321
+ const pathIndex = index > 0 ? `[${index + 1}]` : '';
1322
+ path = '/' + tagName + pathIndex + path;
1323
+
1324
+ current = current.parentNode;
1325
+ }
1326
+
1327
+ return path;
1328
+ }
1329
+ selectors.xpath = getXPath(element);
1330
+
1331
+ // Text-based selector
1332
+ const textContent = element.textContent?.trim();
1333
+ if (textContent && textContent.length > 0 && textContent.length < 50) {
1334
+ selectors.text = textContent;
1335
+ }
1336
+
1337
+ // Role-based selector
1338
+ const role = element.getAttribute('role') || element.getAttribute('aria-role');
1339
+ if (role) {
1340
+ selectors.role = `[role="${role}"]`;
1341
+ }
1342
+
1343
+ // Test ID selectors
1344
+ const testId = element.getAttribute('data-testid') ||
1345
+ element.getAttribute('data-cy') ||
1346
+ element.getAttribute('data-test');
1347
+ if (testId) {
1348
+ selectors.testid = `[data-testid="${testId}"]`;
1349
+ }
1350
+
1351
+ // ARIA label selector
1352
+ const ariaLabel = element.getAttribute('aria-label');
1353
+ if (ariaLabel) {
1354
+ selectors.aria_label = `[aria-label="${ariaLabel}"]`;
1355
+ }
1356
+
1357
+ // Unique CSS selector (most specific)
1358
+ function getUniqueSelector(element) {
1359
+ if (element.id) return '#' + element.id;
1360
+
1361
+ let path = [];
1362
+ let current = element;
1363
+
1364
+ while (current && current !== document.body) {
1365
+ let selector = current.tagName.toLowerCase();
1366
+
1367
+ if (current.className && typeof current.className === 'string') {
1368
+ const classes = current.className.split(' ').filter(c => c.trim());
1369
+ if (classes.length > 0) {
1370
+ selector += '.' + classes.join('.');
1371
+ }
1372
+ }
1373
+
1374
+ // Add nth-child if needed for uniqueness
1375
+ const siblings = Array.from(current.parentNode?.children || [])
1376
+ .filter(sibling => sibling.tagName === current.tagName);
1377
+ if (siblings.length > 1) {
1378
+ const index = siblings.indexOf(current) + 1;
1379
+ selector += `:nth-child(${index})`;
1380
+ }
1381
+
1382
+ path.unshift(selector);
1383
+ current = current.parentElement;
603
1384
  }
604
- path.unshift(selector);
605
- element = element.parentNode;
1385
+
1386
+ return path.join(' > ');
606
1387
  }
607
- return path.join(' > ');
1388
+ selectors.unique_css = getUniqueSelector(element);
1389
+
1390
+ return selectors;
608
1391
  }
609
1392
 
610
- // Helper function to get comprehensive computed styles
1393
+ // Enhanced accessibility analysis
1394
+ function getAccessibilityData(element) {
1395
+ return {
1396
+ // ARIA attributes
1397
+ role: element.getAttribute('role'),
1398
+ aria_label: element.getAttribute('aria-label'),
1399
+ aria_labelledby: element.getAttribute('aria-labelledby'),
1400
+ aria_describedby: element.getAttribute('aria-describedby'),
1401
+ aria_expanded: element.getAttribute('aria-expanded'),
1402
+ aria_hidden: element.getAttribute('aria-hidden'),
1403
+ aria_disabled: element.getAttribute('aria-disabled'),
1404
+ aria_required: element.getAttribute('aria-required'),
1405
+ aria_invalid: element.getAttribute('aria-invalid'),
1406
+ aria_live: element.getAttribute('aria-live'),
1407
+
1408
+ // Keyboard navigation
1409
+ tabindex: element.tabIndex,
1410
+ is_focusable: element.tabIndex >= 0 ||
1411
+ ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'A'].includes(element.tagName),
1412
+
1413
+ // Interactive element detection
1414
+ is_interactive: ['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA'].includes(element.tagName) ||
1415
+ element.hasAttribute('onclick') ||
1416
+ element.getAttribute('role') === 'button' ||
1417
+ element.style.cursor === 'pointer',
1418
+
1419
+ // Form element specifics
1420
+ form_label: element.tagName === 'INPUT' ?
1421
+ document.querySelector(`label[for="${element.id}"]`)?.textContent?.trim() : null,
1422
+
1423
+ // Semantic meaning
1424
+ semantic_role: element.tagName.toLowerCase(),
1425
+ landmark_role: ['HEADER', 'NAV', 'MAIN', 'ASIDE', 'FOOTER'].includes(element.tagName) ?
1426
+ element.tagName.toLowerCase() : element.getAttribute('role')
1427
+ };
1428
+ }
1429
+
1430
+ // Enhanced visual context analysis
1431
+ function getVisualContext(element, computedStyles) {
1432
+ const rect = element.getBoundingClientRect();
1433
+
1434
+ return {
1435
+ bounding_box: {
1436
+ x: Math.round(rect.x),
1437
+ y: Math.round(rect.y),
1438
+ width: Math.round(rect.width),
1439
+ height: Math.round(rect.height),
1440
+ top: Math.round(rect.top),
1441
+ left: Math.round(rect.left),
1442
+ right: Math.round(rect.right),
1443
+ bottom: Math.round(rect.bottom)
1444
+ },
1445
+
1446
+ // Visibility analysis
1447
+ visibility: {
1448
+ is_visible: rect.width > 0 && rect.height > 0 &&
1449
+ computedStyles.display !== 'none' &&
1450
+ computedStyles.visibility !== 'hidden' &&
1451
+ parseFloat(computedStyles.opacity) > 0,
1452
+ is_in_viewport: rect.top < window.innerHeight &&
1453
+ rect.bottom > 0 &&
1454
+ rect.left < window.innerWidth &&
1455
+ rect.right > 0,
1456
+ opacity: parseFloat(computedStyles.opacity),
1457
+ display: computedStyles.display,
1458
+ visibility: computedStyles.visibility
1459
+ },
1460
+
1461
+ // Z-index and layering
1462
+ layering: {
1463
+ z_index: computedStyles.zIndex,
1464
+ position: computedStyles.position,
1465
+ stacking_context: computedStyles.zIndex !== 'auto' ||
1466
+ computedStyles.position === 'fixed' ||
1467
+ computedStyles.position === 'sticky' ||
1468
+ parseFloat(computedStyles.opacity) < 1
1469
+ },
1470
+
1471
+ // Size classification
1472
+ size_category: rect.width === 0 || rect.height === 0 ? 'hidden' :
1473
+ rect.width * rect.height < 100 ? 'tiny' :
1474
+ rect.width * rect.height < 1000 ? 'small' :
1475
+ rect.width * rect.height < 10000 ? 'medium' :
1476
+ rect.width * rect.height < 100000 ? 'large' : 'huge',
1477
+
1478
+ // Visual relationships
1479
+ relationships: {
1480
+ has_children: element.children.length > 0,
1481
+ children_count: element.children.length,
1482
+ parent_tag: element.parentElement?.tagName?.toLowerCase(),
1483
+ siblings_count: element.parentElement?.children?.length - 1 || 0
1484
+ }
1485
+ };
1486
+ }
1487
+
1488
+ // Helper function to get comprehensive computed styles (enhanced)
611
1489
  function getComputedStylesDetailed(element) {
612
1490
  const computed = window.getComputedStyle(element);
613
1491
  return {
@@ -641,6 +1519,8 @@ class BrowserController:
641
1519
  gridTemplateRows: computed.gridTemplateRows,
642
1520
  gridGap: computed.gridGap,
643
1521
  gridArea: computed.gridArea,
1522
+ gridColumn: computed.gridColumn,
1523
+ gridRow: computed.gridRow,
644
1524
 
645
1525
  // Spacing
646
1526
  margin: computed.margin,
@@ -664,6 +1544,8 @@ class BrowserController:
664
1544
  textAlign: computed.textAlign,
665
1545
  textDecoration: computed.textDecoration,
666
1546
  textTransform: computed.textTransform,
1547
+ whiteSpace: computed.whiteSpace,
1548
+ wordWrap: computed.wordWrap,
667
1549
 
668
1550
  // Colors and backgrounds
669
1551
  color: computed.color,
@@ -690,16 +1572,22 @@ class BrowserController:
690
1572
  transform: computed.transform,
691
1573
  transition: computed.transition,
692
1574
  animation: computed.animation,
1575
+ filter: computed.filter,
693
1576
 
694
1577
  // Z-index and overflow
695
1578
  zIndex: computed.zIndex,
696
1579
  overflow: computed.overflow,
697
1580
  overflowX: computed.overflowX,
698
- overflowY: computed.overflowY
1581
+ overflowY: computed.overflowY,
1582
+
1583
+ // Cursor and interaction
1584
+ cursor: computed.cursor,
1585
+ pointerEvents: computed.pointerEvents,
1586
+ userSelect: computed.userSelect
699
1587
  };
700
1588
  }
701
1589
 
702
- // Get all significant elements
1590
+ // Get all significant elements with enhanced analysis
703
1591
  const elements = [];
704
1592
  const selectors = [
705
1593
  // Structural elements
@@ -724,34 +1612,32 @@ class BrowserController:
724
1612
  selectors.forEach(selector => {
725
1613
  try {
726
1614
  document.querySelectorAll(selector).forEach((element, index) => {
727
- const rect = element.getBoundingClientRect();
728
1615
  const computedStyles = getComputedStylesDetailed(element);
1616
+ const visualContext = getVisualContext(element, computedStyles);
729
1617
 
730
- // Only include visible elements with meaningful size
731
- if (rect.width > 0 && rect.height > 0) {
1618
+ // Only include elements with meaningful size or important semantic meaning
1619
+ if (visualContext.bounding_box.width > 0 && visualContext.bounding_box.height > 0 ||
1620
+ ['HEADER', 'NAV', 'MAIN', 'ASIDE', 'FOOTER'].includes(element.tagName)) {
1621
+
732
1622
  elements.push({
1623
+ // Basic element info
733
1624
  selector: selector,
734
1625
  index: index,
735
- uniqueSelector: selector + (index > 0 ? `:nth-of-type(${index + 1})` : ''),
736
- elementPath: getElementPath(element),
737
1626
  tagName: element.tagName.toLowerCase(),
738
1627
  id: element.id || null,
739
1628
  className: element.className || null,
740
1629
  textContent: element.textContent ? element.textContent.trim().substring(0, 200) : null,
741
1630
 
742
- // Bounding box
743
- boundingBox: {
744
- x: Math.round(rect.x),
745
- y: Math.round(rect.y),
746
- width: Math.round(rect.width),
747
- height: Math.round(rect.height),
748
- top: Math.round(rect.top),
749
- left: Math.round(rect.left),
750
- right: Math.round(rect.right),
751
- bottom: Math.round(rect.bottom)
752
- },
1631
+ // v2.0 Enhancement: Multiple selector strategies
1632
+ selectors: generateMultipleSelectors(element),
753
1633
 
754
- // All computed styles
1634
+ // v2.0 Enhancement: Accessibility data
1635
+ accessibility: getAccessibilityData(element),
1636
+
1637
+ // v2.0 Enhancement: Visual context
1638
+ visual_context: visualContext,
1639
+
1640
+ // Enhanced computed styles
755
1641
  computedStyles: computedStyles,
756
1642
 
757
1643
  // Element attributes
@@ -762,16 +1648,7 @@ class BrowserController:
762
1648
 
763
1649
  // Element hierarchy info
764
1650
  childrenCount: element.children.length,
765
- parentTagName: element.parentElement ? element.parentElement.tagName.toLowerCase() : null,
766
-
767
- // Visibility and interaction
768
- isVisible: rect.width > 0 && rect.height > 0 &&
769
- computedStyles.display !== 'none' &&
770
- computedStyles.visibility !== 'hidden' &&
771
- parseFloat(computedStyles.opacity) > 0,
772
- isInteractive: ['button', 'a', 'input', 'select', 'textarea'].includes(element.tagName.toLowerCase()) ||
773
- element.hasAttribute('onclick') ||
774
- element.style.cursor === 'pointer'
1651
+ parentTagName: element.parentElement ? element.parentElement.tagName.toLowerCase() : null
775
1652
  });
776
1653
  }
777
1654
  });
@@ -810,15 +1687,39 @@ class BrowserController:
810
1687
  }
811
1688
  };
812
1689
 
813
- // Analyze page structure
1690
+ // Enhanced page structure analysis
814
1691
  const pageStructure = {
815
1692
  hasHeader: elements.some(el => ['header', 'nav', '.header', '.navbar'].includes(el.selector)),
816
1693
  hasFooter: elements.some(el => ['footer', '.footer'].includes(el.selector)),
817
1694
  hasNavigation: elements.some(el => ['nav', '.nav', '.navbar', '.menu'].includes(el.selector)),
818
1695
  hasSidebar: elements.some(el => ['.sidebar', '.aside', 'aside'].includes(el.selector)),
819
1696
  hasMainContent: elements.some(el => ['main', '.main', '.content'].includes(el.selector)),
820
- interactiveElements: elements.filter(el => el.isInteractive).length,
821
- totalVisibleElements: elements.length
1697
+
1698
+ // v2.0 Enhancement: Accessibility structure
1699
+ accessibilityFeatures: {
1700
+ landmarkElements: elements.filter(el => el.accessibility.landmark_role).length,
1701
+ focusableElements: elements.filter(el => el.accessibility.is_focusable).length,
1702
+ interactiveElements: elements.filter(el => el.accessibility.is_interactive).length,
1703
+ elementsWithAriaLabels: elements.filter(el => el.accessibility.aria_label).length,
1704
+ elementsWithRoles: elements.filter(el => el.accessibility.role).length
1705
+ },
1706
+
1707
+ // v2.0 Enhancement: Visual structure
1708
+ visualFeatures: {
1709
+ visibleElements: elements.filter(el => el.visual_context.visibility.is_visible).length,
1710
+ elementsInViewport: elements.filter(el => el.visual_context.visibility.is_in_viewport).length,
1711
+ layeredElements: elements.filter(el => el.visual_context.layering.stacking_context).length,
1712
+ sizeDistribution: {
1713
+ tiny: elements.filter(el => el.visual_context.size_category === 'tiny').length,
1714
+ small: elements.filter(el => el.visual_context.size_category === 'small').length,
1715
+ medium: elements.filter(el => el.visual_context.size_category === 'medium').length,
1716
+ large: elements.filter(el => el.visual_context.size_category === 'large').length,
1717
+ huge: elements.filter(el => el.visual_context.size_category === 'huge').length
1718
+ }
1719
+ },
1720
+
1721
+ totalVisibleElements: elements.filter(el => el.visual_context.visibility.is_visible).length,
1722
+ totalElements: elements.length
822
1723
  };
823
1724
 
824
1725
  return {
@@ -827,7 +1728,7 @@ class BrowserController:
827
1728
  elements: elements,
828
1729
  totalElements: elements.length,
829
1730
  captureTimestamp: Date.now(),
830
- analysisVersion: "1.0"
1731
+ analysisVersion: "2.0" // v2.0 enhanced analysis
831
1732
  };
832
1733
  }
833
1734
  """)
@@ -835,8 +1736,14 @@ class BrowserController:
835
1736
  return dom_analysis
836
1737
 
837
1738
  except Exception as e:
838
- self.logger.error(f"DOM analysis failed: {e}")
839
- return {"error": str(e), "captureTimestamp": time.time()}
1739
+ self.logger.error(f"Enhanced DOM analysis failed: {e}")
1740
+ return {
1741
+ "error": str(e),
1742
+ "elements": [],
1743
+ "totalElements": 0,
1744
+ "captureTimestamp": time.time(),
1745
+ "analysisVersion": "2.0-error"
1746
+ }
840
1747
 
841
1748
  def _capture_network_data(self) -> Dict[str, Any]:
842
1749
  """Capture comprehensive network request and response data"""
@@ -1132,10 +2039,13 @@ class BrowserController:
1132
2039
  self.logger.error(f"Page state capture failed: {e}")
1133
2040
  return {"error": str(e)}
1134
2041
 
1135
- def _create_analysis_summary(self, dom_analysis: Dict, network_data: Dict, console_data: Dict, performance_data: Dict) -> Dict[str, Any]:
1136
- """Create high-level analysis summary"""
2042
+ def _create_analysis_summary(self, dom_analysis: Dict, network_data: Dict, console_data: Dict, performance_data: Dict,
2043
+ font_analysis: Dict = None, animation_analysis: Dict = None,
2044
+ resource_analysis: Dict = None, storage_analysis: Dict = None) -> Dict[str, Any]:
2045
+ """Enhanced analysis summary with v2.0 comprehensive data"""
1137
2046
  try:
1138
- return {
2047
+ # Base summary (v1.x compatibility)
2048
+ summary = {
1139
2049
  "page_health": {
1140
2050
  "dom_elements_count": dom_analysis.get("totalElements", 0),
1141
2051
  "has_errors": console_data.get("console_summary", {}).get("error_count", 0) > 0,
@@ -1168,8 +2078,73 @@ class BrowserController:
1168
2078
  }
1169
2079
  }
1170
2080
 
2081
+ # v2.0 Enhancements
2082
+ if font_analysis:
2083
+ summary["font_status"] = {
2084
+ "fonts_loaded": font_analysis.get("font_status", {}).get("loaded_fonts", 0),
2085
+ "fonts_loading": font_analysis.get("font_status", {}).get("loading_fonts", 0),
2086
+ "fonts_failed": font_analysis.get("font_status", {}).get("failed_fonts", 0),
2087
+ "loading_complete": font_analysis.get("loading_metrics", {}).get("loading_complete", False),
2088
+ "load_success_rate": font_analysis.get("loading_metrics", {}).get("load_success_rate", "100%"),
2089
+ "used_font_families_count": len(font_analysis.get("used_font_families", []))
2090
+ }
2091
+
2092
+ if animation_analysis:
2093
+ summary["animation_status"] = {
2094
+ "animated_elements": animation_analysis.get("total_animated_elements", 0),
2095
+ "running_animations": animation_analysis.get("running_animations", 0),
2096
+ "running_transitions": animation_analysis.get("running_transitions", 0),
2097
+ "has_active_animations": animation_analysis.get("animation_summary", {}).get("has_active_animations", False),
2098
+ "performance_impact": animation_analysis.get("animation_summary", {}).get("performance_impact", "low"),
2099
+ "animation_stability": animation_analysis.get("animation_summary", {}).get("animation_stability", "stable")
2100
+ }
2101
+
2102
+ if resource_analysis:
2103
+ summary["resource_status"] = {
2104
+ "total_resources": resource_analysis.get("total_resources", 0),
2105
+ "critical_resources": len(resource_analysis.get("critical_resources", [])),
2106
+ "failed_resources": len(resource_analysis.get("failed_resources", [])),
2107
+ "average_load_time_ms": resource_analysis.get("loading_performance", {}).get("average_load_time", 0),
2108
+ "cache_hit_rate": resource_analysis.get("loading_summary", {}).get("resource_efficiency", {}).get("cache_hit_rate", "0%"),
2109
+ "page_load_complete": resource_analysis.get("loading_summary", {}).get("page_load_complete", False),
2110
+ "critical_path_loaded": resource_analysis.get("loading_summary", {}).get("critical_path_loaded", False)
2111
+ }
2112
+
2113
+ if storage_analysis:
2114
+ summary["storage_status"] = {
2115
+ "has_stored_data": storage_analysis.get("storage_summary", {}).get("has_stored_data", False),
2116
+ "storage_types_used": storage_analysis.get("storage_summary", {}).get("storage_types_used", []),
2117
+ "total_estimated_size_bytes": storage_analysis.get("storage_summary", {}).get("total_estimated_size", 0),
2118
+ "has_tracking_cookies": storage_analysis.get("storage_summary", {}).get("privacy_indicators", {}).get("has_tracking_cookies", False),
2119
+ "has_session_data": storage_analysis.get("storage_summary", {}).get("privacy_indicators", {}).get("has_session_data", False),
2120
+ "has_persistent_data": storage_analysis.get("storage_summary", {}).get("privacy_indicators", {}).get("has_persistent_data", False)
2121
+ }
2122
+
2123
+ # v2.0 Enhanced accessibility summary
2124
+ if dom_analysis.get("pageStructure", {}).get("accessibilityFeatures"):
2125
+ summary["accessibility_status"] = {
2126
+ "landmark_elements": dom_analysis.get("pageStructure", {}).get("accessibilityFeatures", {}).get("landmarkElements", 0),
2127
+ "focusable_elements": dom_analysis.get("pageStructure", {}).get("accessibilityFeatures", {}).get("focusableElements", 0),
2128
+ "interactive_elements": dom_analysis.get("pageStructure", {}).get("accessibilityFeatures", {}).get("interactiveElements", 0),
2129
+ "elements_with_aria_labels": dom_analysis.get("pageStructure", {}).get("accessibilityFeatures", {}).get("elementsWithAriaLabels", 0),
2130
+ "elements_with_roles": dom_analysis.get("pageStructure", {}).get("accessibilityFeatures", {}).get("elementsWithRoles", 0),
2131
+ "accessibility_score": self._calculate_accessibility_score(dom_analysis)
2132
+ }
2133
+
2134
+ # v2.0 Enhanced visual summary
2135
+ if dom_analysis.get("pageStructure", {}).get("visualFeatures"):
2136
+ summary["visual_status"] = {
2137
+ "visible_elements": dom_analysis.get("pageStructure", {}).get("visualFeatures", {}).get("visibleElements", 0),
2138
+ "elements_in_viewport": dom_analysis.get("pageStructure", {}).get("visualFeatures", {}).get("elementsInViewport", 0),
2139
+ "layered_elements": dom_analysis.get("pageStructure", {}).get("visualFeatures", {}).get("layeredElements", 0),
2140
+ "size_distribution": dom_analysis.get("pageStructure", {}).get("visualFeatures", {}).get("sizeDistribution", {}),
2141
+ "viewport_utilization": self._calculate_viewport_utilization(dom_analysis)
2142
+ }
2143
+
2144
+ return summary
2145
+
1171
2146
  except Exception as e:
1172
- self.logger.error(f"Analysis summary creation failed: {e}")
2147
+ self.logger.error(f"Enhanced analysis summary creation failed: {e}")
1173
2148
  return {"error": str(e)}
1174
2149
 
1175
2150
  def _calculate_performance_score(self, performance_data: Dict) -> int:
@@ -1202,6 +2177,85 @@ class BrowserController:
1202
2177
 
1203
2178
  except Exception as e:
1204
2179
  return 50 # Default middle score if calculation fails
2180
+
2181
+ def _calculate_accessibility_score(self, dom_analysis: Dict) -> int:
2182
+ """v2.0 Enhancement: Calculate accessibility compliance score (0-100)"""
2183
+ try:
2184
+ score = 100
2185
+ accessibility_features = dom_analysis.get("pageStructure", {}).get("accessibilityFeatures", {})
2186
+ total_elements = dom_analysis.get("totalElements", 1) # Avoid division by zero
2187
+
2188
+ # Deduct points for missing accessibility features
2189
+ landmark_elements = accessibility_features.get("landmarkElements", 0)
2190
+ if landmark_elements == 0:
2191
+ score -= 20 # No semantic landmarks
2192
+
2193
+ focusable_elements = accessibility_features.get("focusableElements", 0)
2194
+ interactive_elements = accessibility_features.get("interactiveElements", 0)
2195
+ if interactive_elements > 0 and focusable_elements == 0:
2196
+ score -= 30 # Interactive elements but none focusable
2197
+
2198
+ elements_with_aria_labels = accessibility_features.get("elementsWithAriaLabels", 0)
2199
+ if interactive_elements > 0 and elements_with_aria_labels == 0:
2200
+ score -= 25 # Interactive elements without ARIA labels
2201
+
2202
+ elements_with_roles = accessibility_features.get("elementsWithRoles", 0)
2203
+ role_coverage = elements_with_roles / total_elements if total_elements > 0 else 0
2204
+ if role_coverage < 0.1: # Less than 10% of elements have roles
2205
+ score -= 15
2206
+
2207
+ # Bonus points for good accessibility practices
2208
+ if landmark_elements > 3: # Good semantic structure
2209
+ score += 5
2210
+ if role_coverage > 0.5: # Excellent role coverage
2211
+ score += 5
2212
+
2213
+ return max(0, min(100, score))
2214
+
2215
+ except Exception as e:
2216
+ self.logger.error(f"Accessibility score calculation failed: {e}")
2217
+ return 50 # Default middle score
2218
+
2219
+ def _calculate_viewport_utilization(self, dom_analysis: Dict) -> Dict[str, Any]:
2220
+ """v2.0 Enhancement: Calculate how well the viewport is utilized"""
2221
+ try:
2222
+ visual_features = dom_analysis.get("pageStructure", {}).get("visualFeatures", {})
2223
+ page_info = dom_analysis.get("pageInfo", {})
2224
+
2225
+ visible_elements = visual_features.get("visibleElements", 0)
2226
+ elements_in_viewport = visual_features.get("elementsInViewport", 0)
2227
+ viewport_height = page_info.get("viewport", {}).get("height", 1) # Avoid division by zero
2228
+ document_height = page_info.get("documentSize", {}).get("height", viewport_height)
2229
+
2230
+ # Calculate utilization metrics
2231
+ viewport_fill_ratio = elements_in_viewport / visible_elements if visible_elements > 0 else 0
2232
+ content_density = visible_elements / viewport_height * 1000 if viewport_height > 0 else 0 # Elements per 1000px
2233
+ scroll_ratio = document_height / viewport_height if viewport_height > 0 else 1
2234
+
2235
+ # Determine utilization quality
2236
+ utilization_quality = "excellent" if viewport_fill_ratio > 0.8 and content_density > 2 else \
2237
+ "good" if viewport_fill_ratio > 0.6 and content_density > 1 else \
2238
+ "fair" if viewport_fill_ratio > 0.4 else "poor"
2239
+
2240
+ return {
2241
+ "viewport_fill_ratio": round(viewport_fill_ratio, 2),
2242
+ "content_density_per_1000px": round(content_density, 1),
2243
+ "scroll_ratio": round(scroll_ratio, 1),
2244
+ "utilization_quality": utilization_quality,
2245
+ "elements_in_viewport": elements_in_viewport,
2246
+ "total_visible_elements": visible_elements
2247
+ }
2248
+
2249
+ except Exception as e:
2250
+ self.logger.error(f"Viewport utilization calculation failed: {e}")
2251
+ return {
2252
+ "viewport_fill_ratio": 0.5,
2253
+ "content_density_per_1000px": 1.0,
2254
+ "scroll_ratio": 1.0,
2255
+ "utilization_quality": "unknown",
2256
+ "elements_in_viewport": 0,
2257
+ "total_visible_elements": 0
2258
+ }
1205
2259
 
1206
2260
  def get_collected_data(self) -> Dict:
1207
2261
  """Get all collected browser data"""
@@ -1218,3 +2272,202 @@ class BrowserController:
1218
2272
  "failed_requests": len(self.get_failed_requests())
1219
2273
  }
1220
2274
  }
2275
+
2276
+ # v2.0 Enhancement: Hot Reload Intelligence Methods
2277
+
2278
+ async def start_hmr_monitoring(self) -> Dict[str, Any]:
2279
+ """
2280
+ Start Hot Module Replacement monitoring for precision CSS iteration
2281
+
2282
+ This enables the breakthrough v2.0 feature: precise timing instead of arbitrary waits
2283
+
2284
+ Returns:
2285
+ Status dict with framework detection and monitoring state
2286
+ """
2287
+ try:
2288
+ self.logger.info("🔥 Starting Hot Reload Intelligence monitoring...")
2289
+
2290
+ # Auto-detect framework
2291
+ detected_framework = await self.hmr_detector.auto_detect_framework()
2292
+
2293
+ if detected_framework:
2294
+ # Start monitoring
2295
+ success = await self.hmr_detector.start_monitoring()
2296
+
2297
+ if success:
2298
+ self.hmr_monitoring_active = True
2299
+ framework_info = self.hmr_detector.get_framework_info()
2300
+
2301
+ self.logger.info(f"✅ HMR monitoring active for {framework_info['name']}")
2302
+
2303
+ return {
2304
+ 'success': True,
2305
+ 'framework_detected': True,
2306
+ 'framework': framework_info,
2307
+ 'monitoring_active': True,
2308
+ 'message': f"Hot reload monitoring started for {framework_info['name']}"
2309
+ }
2310
+ else:
2311
+ return {
2312
+ 'success': False,
2313
+ 'framework_detected': True,
2314
+ 'framework': self.hmr_detector.get_framework_info(),
2315
+ 'monitoring_active': False,
2316
+ 'message': 'Framework detected but monitoring failed to start'
2317
+ }
2318
+ else:
2319
+ return {
2320
+ 'success': False,
2321
+ 'framework_detected': False,
2322
+ 'framework': None,
2323
+ 'monitoring_active': False,
2324
+ 'message': 'No HMR framework detected - using fallback timing',
2325
+ 'supported_frameworks': ['Vite', 'Webpack Dev Server', 'Next.js', 'Parcel', 'Laravel Mix']
2326
+ }
2327
+
2328
+ except Exception as e:
2329
+ self.logger.error(f"HMR monitoring startup failed: {e}")
2330
+ return {
2331
+ 'success': False,
2332
+ 'framework_detected': False,
2333
+ 'framework': None,
2334
+ 'monitoring_active': False,
2335
+ 'error': str(e),
2336
+ 'message': 'HMR monitoring failed to start'
2337
+ }
2338
+
2339
+ async def wait_for_css_update(self, timeout: float = 10.0) -> Dict[str, Any]:
2340
+ """
2341
+ Wait for CSS update with precision timing - the key v2.0 breakthrough method
2342
+
2343
+ This replaces arbitrary waits with precise HMR event detection:
2344
+
2345
+ OLD WAY (unreliable):
2346
+ await page.screenshot("before.png")
2347
+ # ... developer makes CSS changes ...
2348
+ await page.wait_for_timeout(2000) # Arbitrary wait - too short or too long
2349
+ await page.screenshot("after.png")
2350
+
2351
+ NEW WAY (precise):
2352
+ await page.screenshot("before.png")
2353
+ # ... developer makes CSS changes ...
2354
+ result = await browser_controller.wait_for_css_update() # Exact timing
2355
+ await page.screenshot("after.png")
2356
+
2357
+ Args:
2358
+ timeout: Maximum time to wait for CSS update (seconds)
2359
+
2360
+ Returns:
2361
+ Dict with timing results and HMR event data
2362
+ """
2363
+ if not self.hmr_monitoring_active:
2364
+ # Fallback to traditional wait with warning
2365
+ self.logger.warning("⚠️ HMR monitoring not active - using fallback timing")
2366
+ await asyncio.sleep(2.0) # Default fallback wait
2367
+ return {
2368
+ 'method': 'fallback_timing',
2369
+ 'wait_time': 2.0,
2370
+ 'hmr_event': None,
2371
+ 'precision_timing': False,
2372
+ 'message': 'Used fallback timing - consider starting HMR monitoring for precision'
2373
+ }
2374
+
2375
+ start_time = time.time()
2376
+ hmr_event = await self.hmr_detector.wait_for_css_update(timeout)
2377
+ actual_wait_time = time.time() - start_time
2378
+
2379
+ if hmr_event:
2380
+ self.logger.info(f"✅ CSS update detected after {actual_wait_time:.2f}s - precision timing achieved")
2381
+ return {
2382
+ 'method': 'hmr_precision_timing',
2383
+ 'wait_time': actual_wait_time,
2384
+ 'hmr_event': hmr_event,
2385
+ 'precision_timing': True,
2386
+ 'framework': hmr_event['framework'],
2387
+ 'event_type': hmr_event['event_type'],
2388
+ 'message': f"CSS update detected via {hmr_event['framework']} HMR"
2389
+ }
2390
+ else:
2391
+ self.logger.warning(f"⏰ CSS update timeout after {timeout}s - no HMR event detected")
2392
+ return {
2393
+ 'method': 'hmr_timeout',
2394
+ 'wait_time': actual_wait_time,
2395
+ 'hmr_event': None,
2396
+ 'precision_timing': False,
2397
+ 'timeout': timeout,
2398
+ 'message': f'No CSS update detected within {timeout}s timeout'
2399
+ }
2400
+
2401
+ def get_hmr_status(self) -> Dict[str, Any]:
2402
+ """Get current Hot Reload Intelligence status"""
2403
+ base_status = {
2404
+ 'hmr_monitoring_active': self.hmr_monitoring_active,
2405
+ 'browser_controller_ready': self.page is not None
2406
+ }
2407
+
2408
+ if hasattr(self, 'hmr_detector'):
2409
+ hmr_status = self.hmr_detector.get_hmr_status()
2410
+ framework_info = self.hmr_detector.get_framework_info()
2411
+
2412
+ return {
2413
+ **base_status,
2414
+ **hmr_status,
2415
+ 'framework_info': framework_info,
2416
+ 'capabilities': {
2417
+ 'precision_css_timing': self.hmr_monitoring_active,
2418
+ 'build_completion_detection': self.hmr_monitoring_active,
2419
+ 'framework_auto_detection': True,
2420
+ 'supported_frameworks': ['Vite', 'Webpack Dev Server', 'Next.js', 'Parcel', 'Laravel Mix']
2421
+ }
2422
+ }
2423
+ else:
2424
+ return {
2425
+ **base_status,
2426
+ 'error': 'HMR detector not initialized'
2427
+ }
2428
+
2429
+ async def stop_hmr_monitoring(self):
2430
+ """Stop Hot Reload Intelligence monitoring"""
2431
+ if hasattr(self, 'hmr_detector') and self.hmr_monitoring_active:
2432
+ await self.hmr_detector.stop_monitoring()
2433
+ self.hmr_monitoring_active = False
2434
+ self.logger.info("🔥 Hot Reload Intelligence monitoring stopped")
2435
+
2436
+ # v2.0 Enhancement: Error Context Collection Methods
2437
+
2438
+ async def _collect_error_context_async(self, error_event: Dict[str, Any]):
2439
+ """Asynchronously collect error context without blocking event handlers"""
2440
+ try:
2441
+ if self.error_context_collector:
2442
+ context_data = await self.error_context_collector.capture_error_context(error_event)
2443
+ self.logger.debug(f"📊 Error context collected for {error_event.get('type')}")
2444
+ except Exception as e:
2445
+ self.logger.error(f"Error context collection failed: {e}")
2446
+
2447
+ def record_browser_action(self, action_type: str, details: Dict = None):
2448
+ """Record browser action for error correlation"""
2449
+ if self.error_context_collector:
2450
+ self.error_context_collector.record_action(action_type, details)
2451
+
2452
+ async def capture_interaction_error_context(self, action: str, target: str, error: Exception) -> Dict[str, Any]:
2453
+ """Capture context for interaction failures (clicks, fills, etc.)"""
2454
+ if not self.error_context_collector:
2455
+ return {'error': 'Error context collector not initialized'}
2456
+
2457
+ error_event = {
2458
+ 'type': 'interaction_error',
2459
+ 'action': action,
2460
+ 'target': target,
2461
+ 'error_message': str(error),
2462
+ 'selector': target,
2463
+ 'timestamp': time.time()
2464
+ }
2465
+
2466
+ return await self.error_context_collector.capture_error_context(error_event)
2467
+
2468
+ def get_error_context_summary(self) -> Dict[str, Any]:
2469
+ """Get summary of collected error contexts"""
2470
+ if not self.error_context_collector:
2471
+ return {'error': 'Error context collector not initialized'}
2472
+
2473
+ return self.error_context_collector.get_error_context_summary()