cursorflow 1.2.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.
@@ -0,0 +1,386 @@
1
+ """
2
+ Universal Browser Engine
3
+
4
+ Framework-agnostic browser automation using Playwright.
5
+ Adapts to different web architectures through pluggable adapters.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import time
11
+ from typing import Dict, List, Any, Optional
12
+ from playwright.async_api import async_playwright, Page, Browser, BrowserContext
13
+ import logging
14
+
15
+ class BrowserEngine:
16
+ """Universal browser automation engine"""
17
+
18
+ def __init__(self, base_url: str, adapter):
19
+ self.base_url = base_url
20
+ self.adapter = adapter
21
+ self.browser = None
22
+ self.context = None
23
+ self.page = None
24
+
25
+ # Event tracking
26
+ self.events = []
27
+ self.network_requests = []
28
+ self.console_errors = []
29
+
30
+ self.logger = logging.getLogger(__name__)
31
+
32
+ async def initialize(self, headless: bool = True, device: str = 'desktop'):
33
+ """Initialize browser with framework-appropriate settings"""
34
+
35
+ playwright = await async_playwright().start()
36
+
37
+ # Browser configuration
38
+ browser_config = {
39
+ 'headless': headless,
40
+ 'args': ['--disable-web-security', '--disable-features=VizDisplayCompositor']
41
+ }
42
+
43
+ # Add framework-specific browser settings
44
+ framework_config = self.adapter.get_browser_config()
45
+ browser_config.update(framework_config)
46
+
47
+ self.browser = await playwright.chromium.launch(**browser_config)
48
+
49
+ # Create context with device settings
50
+ context_config = {
51
+ 'viewport': {'width': 1440, 'height': 900},
52
+ 'user_agent': 'CursorTestingAgent/1.0'
53
+ }
54
+
55
+ if device == 'mobile':
56
+ context_config['viewport'] = {'width': 375, 'height': 667}
57
+ elif device == 'tablet':
58
+ context_config['viewport'] = {'width': 768, 'height': 1024}
59
+
60
+ self.context = await self.browser.new_context(**context_config)
61
+ self.page = await self.context.new_page()
62
+
63
+ # Setup event listeners
64
+ await self._setup_event_listeners()
65
+
66
+ self.logger.info(f"Browser initialized for {self.adapter.__class__.__name__}")
67
+
68
+ async def _setup_event_listeners(self):
69
+ """Setup browser event monitoring"""
70
+
71
+ # Console message handler
72
+ self.page.on('console', lambda msg: self._handle_console_message(msg))
73
+
74
+ # Request/response handler
75
+ self.page.on('request', lambda req: self._handle_request(req))
76
+ self.page.on('response', lambda resp: self._handle_response(resp))
77
+
78
+ # Error handlers
79
+ self.page.on('pageerror', lambda err: self._handle_page_error(err))
80
+ self.page.on('requestfailed', lambda req: self._handle_request_failed(req))
81
+
82
+ def _handle_console_message(self, message):
83
+ """Handle browser console messages"""
84
+ event = {
85
+ 'timestamp': time.time(),
86
+ 'type': 'console',
87
+ 'level': message.type,
88
+ 'text': message.text,
89
+ 'location': message.location
90
+ }
91
+
92
+ self.events.append(event)
93
+
94
+ if message.type in ['error', 'warning']:
95
+ self.console_errors.append(event)
96
+
97
+ def _handle_request(self, request):
98
+ """Handle network requests"""
99
+ self.network_requests.append({
100
+ 'timestamp': time.time(),
101
+ 'type': 'request',
102
+ 'method': request.method,
103
+ 'url': request.url,
104
+ 'headers': dict(request.headers),
105
+ 'post_data': request.post_data
106
+ })
107
+
108
+ def _handle_response(self, response):
109
+ """Handle network responses"""
110
+ self.network_requests.append({
111
+ 'timestamp': time.time(),
112
+ 'type': 'response',
113
+ 'status': response.status,
114
+ 'url': response.url,
115
+ 'headers': dict(response.headers)
116
+ })
117
+
118
+ def _handle_page_error(self, error):
119
+ """Handle JavaScript page errors"""
120
+ self.events.append({
121
+ 'timestamp': time.time(),
122
+ 'type': 'page_error',
123
+ 'message': str(error),
124
+ 'severity': 'critical'
125
+ })
126
+
127
+ def _handle_request_failed(self, request):
128
+ """Handle failed network requests"""
129
+ self.events.append({
130
+ 'timestamp': time.time(),
131
+ 'type': 'request_failed',
132
+ 'url': request.url,
133
+ 'method': request.method,
134
+ 'failure_text': request.failure,
135
+ 'severity': 'high'
136
+ })
137
+
138
+ async def navigate(self, url: str, params: Optional[Dict] = None):
139
+ """Navigate to URL with framework-appropriate handling"""
140
+
141
+ # Build full URL using adapter
142
+ full_url = self.adapter.build_url(self.base_url, url, params)
143
+
144
+ self.logger.info(f"Navigating to: {full_url}")
145
+
146
+ # Record navigation event
147
+ self.events.append({
148
+ 'timestamp': time.time(),
149
+ 'type': 'navigation',
150
+ 'url': full_url,
151
+ 'params': params
152
+ })
153
+
154
+ # Navigate
155
+ await self.page.goto(full_url)
156
+
157
+ # Wait for framework-specific ready state
158
+ await self.adapter.wait_for_ready_state(self.page)
159
+
160
+ # Capture initial state
161
+ await self._capture_page_state('navigation_complete')
162
+
163
+ async def execute_workflow(self, workflow_definition: List[Dict]) -> Dict:
164
+ """Execute a defined workflow of actions"""
165
+
166
+ workflow_results = {
167
+ 'actions': [],
168
+ 'success': True,
169
+ 'errors': []
170
+ }
171
+
172
+ for step in workflow_definition:
173
+ try:
174
+ action_result = await self._execute_action(step)
175
+ workflow_results['actions'].append(action_result)
176
+
177
+ if not action_result.get('success', True):
178
+ workflow_results['success'] = False
179
+
180
+ except Exception as e:
181
+ error = {
182
+ 'action': step,
183
+ 'error': str(e),
184
+ 'timestamp': time.time()
185
+ }
186
+ workflow_results['errors'].append(error)
187
+ workflow_results['success'] = False
188
+ self.logger.error(f"Workflow step failed: {e}")
189
+
190
+ return workflow_results
191
+
192
+ async def _execute_action(self, action: Dict) -> Dict:
193
+ """Execute a single test action"""
194
+
195
+ action_type = action.get('type') or list(action.keys())[0]
196
+ action_config = action.get(action_type, action)
197
+
198
+ start_time = time.time()
199
+
200
+ # Record action start
201
+ self.events.append({
202
+ 'timestamp': start_time,
203
+ 'type': 'action_start',
204
+ 'action': action_type,
205
+ 'config': action_config
206
+ })
207
+
208
+ result = {'action': action_type, 'success': True}
209
+
210
+ try:
211
+ if action_type == 'click':
212
+ await self.page.click(action_config['selector'])
213
+
214
+ elif action_type == 'fill':
215
+ await self.page.fill(action_config['selector'], action_config['value'])
216
+
217
+ elif action_type == 'select':
218
+ await self.page.select_option(action_config['selector'], action_config['value'])
219
+
220
+ elif action_type == 'wait_for':
221
+ await self.page.wait_for_selector(action_config['selector'])
222
+
223
+ elif action_type == 'wait_for_condition':
224
+ await self.page.wait_for_function(action_config['condition'])
225
+
226
+ elif action_type == 'capture':
227
+ await self._capture_page_state(action_config['name'])
228
+
229
+ elif action_type == 'validate':
230
+ validation_result = await self._validate_condition(action_config)
231
+ result['validation'] = validation_result
232
+ result['success'] = validation_result['passed']
233
+
234
+ elif action_type == 'wait':
235
+ wait_time = action_config.get('timeout', 1000)
236
+ await self.page.wait_for_timeout(wait_time)
237
+
238
+ else:
239
+ # Let adapter handle framework-specific actions
240
+ await self.adapter.execute_custom_action(self.page, action_type, action_config)
241
+
242
+ except Exception as e:
243
+ result['success'] = False
244
+ result['error'] = str(e)
245
+ self.logger.error(f"Action {action_type} failed: {e}")
246
+
247
+ # Record completion
248
+ result['duration'] = time.time() - start_time
249
+
250
+ self.events.append({
251
+ 'timestamp': time.time(),
252
+ 'type': 'action_complete',
253
+ 'action': action_type,
254
+ 'result': result
255
+ })
256
+
257
+ return result
258
+
259
+ async def _capture_page_state(self, name: str):
260
+ """Capture current page state for debugging"""
261
+
262
+ state = {
263
+ 'timestamp': time.time(),
264
+ 'name': name,
265
+ 'url': self.page.url,
266
+ 'title': await self.page.title(),
267
+ 'screenshot': f"screenshots/{name}_{int(time.time())}.png",
268
+ 'dom_snapshot': await self.page.content(),
269
+ 'local_storage': await self.page.evaluate('() => JSON.stringify(localStorage)'),
270
+ 'session_storage': await self.page.evaluate('() => JSON.stringify(sessionStorage)')
271
+ }
272
+
273
+ # Take screenshot
274
+ await self.page.screenshot(path=state['screenshot'])
275
+
276
+ # Framework-specific state capture
277
+ framework_state = await self.adapter.capture_framework_state(self.page)
278
+ state['framework_data'] = framework_state
279
+
280
+ self.events.append({
281
+ 'timestamp': time.time(),
282
+ 'type': 'state_capture',
283
+ 'state': state
284
+ })
285
+
286
+ return state
287
+
288
+ async def _validate_condition(self, validation_config: Dict) -> Dict:
289
+ """Validate page conditions"""
290
+
291
+ validation_result = {
292
+ 'passed': True,
293
+ 'checks': []
294
+ }
295
+
296
+ # Standard validations
297
+ if 'selector' in validation_config:
298
+ selector = validation_config['selector']
299
+
300
+ # Check existence
301
+ if validation_config.get('exists') is not None:
302
+ exists = await self.page.is_visible(selector)
303
+ expected = validation_config['exists']
304
+ passed = exists == expected
305
+
306
+ validation_result['checks'].append({
307
+ 'type': 'exists',
308
+ 'selector': selector,
309
+ 'expected': expected,
310
+ 'actual': exists,
311
+ 'passed': passed
312
+ })
313
+
314
+ if not passed:
315
+ validation_result['passed'] = False
316
+
317
+ # Check text content
318
+ if 'text_contains' in validation_config:
319
+ text = await self.page.text_content(selector)
320
+ expected = validation_config['text_contains']
321
+ passed = expected in (text or '')
322
+
323
+ validation_result['checks'].append({
324
+ 'type': 'text_contains',
325
+ 'selector': selector,
326
+ 'expected': expected,
327
+ 'actual': text,
328
+ 'passed': passed
329
+ })
330
+
331
+ if not passed:
332
+ validation_result['passed'] = False
333
+
334
+ # Framework-specific validations
335
+ framework_validations = await self.adapter.validate_framework_conditions(
336
+ self.page, validation_config
337
+ )
338
+ validation_result['framework_checks'] = framework_validations
339
+
340
+ return validation_result
341
+
342
+ async def get_performance_metrics(self) -> Dict:
343
+ """Get browser performance metrics"""
344
+
345
+ metrics = await self.page.evaluate("""() => {
346
+ const timing = performance.timing;
347
+ const navigation = performance.getEntriesByType('navigation')[0];
348
+
349
+ return {
350
+ page_load_time: timing.loadEventEnd - timing.navigationStart,
351
+ dom_ready_time: timing.domContentLoadedEventEnd - timing.navigationStart,
352
+ first_paint: navigation ? navigation.loadEventEnd : null,
353
+ resource_count: performance.getEntriesByType('resource').length,
354
+ memory_usage: performance.memory ? {
355
+ used: performance.memory.usedJSHeapSize,
356
+ total: performance.memory.totalJSHeapSize,
357
+ limit: performance.memory.jsHeapSizeLimit
358
+ } : null
359
+ };
360
+ }""")
361
+
362
+ return metrics
363
+
364
+ def get_events(self) -> List[Dict]:
365
+ """Get all recorded browser events"""
366
+ return self.events.copy()
367
+
368
+ def get_console_errors(self) -> List[Dict]:
369
+ """Get browser console errors"""
370
+ return self.console_errors.copy()
371
+
372
+ def get_network_requests(self) -> List[Dict]:
373
+ """Get network request/response data"""
374
+ return self.network_requests.copy()
375
+
376
+ async def cleanup(self):
377
+ """Clean up browser resources"""
378
+
379
+ if self.page:
380
+ await self.page.close()
381
+ if self.context:
382
+ await self.context.close()
383
+ if self.browser:
384
+ await self.browser.close()
385
+
386
+ self.logger.info("Browser cleanup complete")