cursorflow 1.3.6__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.
- cursorflow/core/browser_controller.py +1367 -80
- cursorflow/core/error_context_collector.py +590 -0
- cursorflow/core/hmr_detector.py +439 -0
- cursorflow/core/mockup_comparator.py +32 -6
- cursorflow/core/trace_manager.py +209 -0
- cursorflow/log_sources/local_file.py +5 -1
- cursorflow/log_sources/ssh_remote.py +7 -2
- cursorflow-2.0.0.dist-info/METADATA +293 -0
- {cursorflow-1.3.6.dist-info → cursorflow-2.0.0.dist-info}/RECORD +13 -10
- cursorflow-1.3.6.dist-info/METADATA +0 -247
- {cursorflow-1.3.6.dist-info → cursorflow-2.0.0.dist-info}/WHEEL +0 -0
- {cursorflow-1.3.6.dist-info → cursorflow-2.0.0.dist-info}/entry_points.txt +0 -0
- {cursorflow-1.3.6.dist-info → cursorflow-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-1.3.6.dist-info → cursorflow-2.0.0.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
54
|
-
|
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
|
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.
|
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
|
-
"""
|
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
|
-
#
|
572
|
-
|
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"
|
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
|
-
"""
|
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
|
-
//
|
595
|
-
function
|
596
|
-
const
|
597
|
-
|
598
|
-
|
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
|
-
|
601
|
-
}
|
602
|
-
|
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
|
-
|
605
|
-
|
1385
|
+
|
1386
|
+
return path.join(' > ');
|
606
1387
|
}
|
607
|
-
|
1388
|
+
selectors.unique_css = getUniqueSelector(element);
|
1389
|
+
|
1390
|
+
return selectors;
|
1391
|
+
}
|
1392
|
+
|
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
|
+
};
|
608
1486
|
}
|
609
1487
|
|
610
|
-
// Helper function to get comprehensive computed styles
|
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
|
731
|
-
if (
|
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
|
-
//
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
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),
|
1633
|
+
|
1634
|
+
// v2.0 Enhancement: Accessibility data
|
1635
|
+
accessibility: getAccessibilityData(element),
|
753
1636
|
|
754
|
-
//
|
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
|
-
//
|
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
|
-
|
821
|
-
|
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: "
|
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 {
|
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"""
|
@@ -982,6 +1889,13 @@ class BrowserController:
|
|
982
1889
|
const paint = perf.getEntriesByType('paint');
|
983
1890
|
const resources = perf.getEntriesByType('resource');
|
984
1891
|
|
1892
|
+
// Helper function to safely calculate timing differences
|
1893
|
+
const safeTiming = (end, start) => {
|
1894
|
+
if (!end || !start || end === 0 || start === 0) return null;
|
1895
|
+
const diff = end - start;
|
1896
|
+
return diff >= 0 ? diff : null;
|
1897
|
+
};
|
1898
|
+
|
985
1899
|
// Memory usage (if available)
|
986
1900
|
const memory = performance.memory ? {
|
987
1901
|
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
@@ -1000,19 +1914,40 @@ class BrowserController:
|
|
1000
1914
|
|
1001
1915
|
return {
|
1002
1916
|
navigation: navigation ? {
|
1003
|
-
domContentLoaded: navigation.domContentLoadedEventEnd
|
1004
|
-
loadComplete: navigation.loadEventEnd
|
1005
|
-
domInteractive: navigation.domInteractive
|
1006
|
-
domComplete: navigation.domComplete
|
1007
|
-
redirectTime: navigation.redirectEnd
|
1008
|
-
dnsTime: navigation.domainLookupEnd
|
1009
|
-
connectTime: navigation.connectEnd
|
1010
|
-
requestTime: navigation.responseStart
|
1011
|
-
responseTime: navigation.responseEnd
|
1012
|
-
|
1917
|
+
domContentLoaded: safeTiming(navigation.domContentLoadedEventEnd, navigation.domContentLoadedEventStart),
|
1918
|
+
loadComplete: safeTiming(navigation.loadEventEnd, navigation.loadEventStart),
|
1919
|
+
domInteractive: safeTiming(navigation.domInteractive, navigation.navigationStart),
|
1920
|
+
domComplete: safeTiming(navigation.domComplete, navigation.navigationStart),
|
1921
|
+
redirectTime: safeTiming(navigation.redirectEnd, navigation.redirectStart),
|
1922
|
+
dnsTime: safeTiming(navigation.domainLookupEnd, navigation.domainLookupStart),
|
1923
|
+
connectTime: safeTiming(navigation.connectEnd, navigation.connectStart),
|
1924
|
+
requestTime: safeTiming(navigation.responseStart, navigation.requestStart),
|
1925
|
+
responseTime: safeTiming(navigation.responseEnd, navigation.responseStart),
|
1926
|
+
// Add raw values for debugging
|
1927
|
+
_raw: {
|
1928
|
+
navigationStart: navigation.navigationStart,
|
1929
|
+
domContentLoadedEventStart: navigation.domContentLoadedEventStart,
|
1930
|
+
domContentLoadedEventEnd: navigation.domContentLoadedEventEnd,
|
1931
|
+
loadEventStart: navigation.loadEventStart,
|
1932
|
+
loadEventEnd: navigation.loadEventEnd
|
1933
|
+
}
|
1934
|
+
} : {
|
1935
|
+
domContentLoaded: null,
|
1936
|
+
loadComplete: null,
|
1937
|
+
domInteractive: null,
|
1938
|
+
domComplete: null,
|
1939
|
+
redirectTime: null,
|
1940
|
+
dnsTime: null,
|
1941
|
+
connectTime: null,
|
1942
|
+
requestTime: null,
|
1943
|
+
responseTime: null,
|
1944
|
+
_raw: null,
|
1945
|
+
_note: "Navigation timing not available (likely headless mode)"
|
1946
|
+
},
|
1013
1947
|
paint: {
|
1014
|
-
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime ||
|
1015
|
-
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime ||
|
1948
|
+
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || null,
|
1949
|
+
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || null,
|
1950
|
+
_available: paint.length > 0
|
1016
1951
|
},
|
1017
1952
|
memory: memory,
|
1018
1953
|
resources: {
|
@@ -1036,12 +1971,18 @@ class BrowserController:
|
|
1036
1971
|
"browser_metrics": browser_metrics,
|
1037
1972
|
"detailed_metrics": additional_metrics,
|
1038
1973
|
"performance_summary": {
|
1039
|
-
"page_load_time": additional_metrics.get("navigation", {}).get("loadComplete"
|
1040
|
-
"dom_content_loaded": additional_metrics.get("navigation", {}).get("domContentLoaded"
|
1041
|
-
"first_paint": additional_metrics.get("paint", {}).get("firstPaint"
|
1042
|
-
"first_contentful_paint": additional_metrics.get("paint", {}).get("firstContentfulPaint"
|
1974
|
+
"page_load_time": additional_metrics.get("navigation", {}).get("loadComplete"),
|
1975
|
+
"dom_content_loaded": additional_metrics.get("navigation", {}).get("domContentLoaded"),
|
1976
|
+
"first_paint": additional_metrics.get("paint", {}).get("firstPaint"),
|
1977
|
+
"first_contentful_paint": additional_metrics.get("paint", {}).get("firstContentfulPaint"),
|
1043
1978
|
"total_resources": additional_metrics.get("resources", {}).get("summary", {}).get("totalResources", 0),
|
1044
|
-
"memory_usage_mb": additional_metrics.get("memory", {}).get("usedJSHeapSize", 0) / (1024 * 1024) if additional_metrics.get("memory") else None
|
1979
|
+
"memory_usage_mb": additional_metrics.get("memory", {}).get("usedJSHeapSize", 0) / (1024 * 1024) if additional_metrics.get("memory") else None,
|
1980
|
+
"_reliability": {
|
1981
|
+
"navigation_timing_available": additional_metrics.get("navigation", {}).get("_raw") is not None,
|
1982
|
+
"paint_timing_available": additional_metrics.get("paint", {}).get("_available", False),
|
1983
|
+
"memory_available": additional_metrics.get("memory") is not None,
|
1984
|
+
"note": "Some metrics may be null in headless mode - this is expected behavior"
|
1985
|
+
}
|
1045
1986
|
}
|
1046
1987
|
}
|
1047
1988
|
|
@@ -1098,10 +2039,13 @@ class BrowserController:
|
|
1098
2039
|
self.logger.error(f"Page state capture failed: {e}")
|
1099
2040
|
return {"error": str(e)}
|
1100
2041
|
|
1101
|
-
def _create_analysis_summary(self, dom_analysis: Dict, network_data: Dict, console_data: Dict, performance_data: Dict
|
1102
|
-
|
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"""
|
1103
2046
|
try:
|
1104
|
-
|
2047
|
+
# Base summary (v1.x compatibility)
|
2048
|
+
summary = {
|
1105
2049
|
"page_health": {
|
1106
2050
|
"dom_elements_count": dom_analysis.get("totalElements", 0),
|
1107
2051
|
"has_errors": console_data.get("console_summary", {}).get("error_count", 0) > 0,
|
@@ -1134,8 +2078,73 @@ class BrowserController:
|
|
1134
2078
|
}
|
1135
2079
|
}
|
1136
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
|
+
|
1137
2146
|
except Exception as e:
|
1138
|
-
self.logger.error(f"
|
2147
|
+
self.logger.error(f"Enhanced analysis summary creation failed: {e}")
|
1139
2148
|
return {"error": str(e)}
|
1140
2149
|
|
1141
2150
|
def _calculate_performance_score(self, performance_data: Dict) -> int:
|
@@ -1168,6 +2177,85 @@ class BrowserController:
|
|
1168
2177
|
|
1169
2178
|
except Exception as e:
|
1170
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
|
+
}
|
1171
2259
|
|
1172
2260
|
def get_collected_data(self) -> Dict:
|
1173
2261
|
"""Get all collected browser data"""
|
@@ -1184,3 +2272,202 @@ class BrowserController:
|
|
1184
2272
|
"failed_requests": len(self.get_failed_requests())
|
1185
2273
|
}
|
1186
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()
|