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.
- cursorflow/__init__.py +78 -0
- cursorflow/auto_updater.py +244 -0
- cursorflow/cli.py +408 -0
- cursorflow/core/agent.py +272 -0
- cursorflow/core/auth_handler.py +433 -0
- cursorflow/core/browser_controller.py +534 -0
- cursorflow/core/browser_engine.py +386 -0
- cursorflow/core/css_iterator.py +397 -0
- cursorflow/core/cursor_integration.py +744 -0
- cursorflow/core/cursorflow.py +649 -0
- cursorflow/core/error_correlator.py +322 -0
- cursorflow/core/event_correlator.py +182 -0
- cursorflow/core/file_change_monitor.py +548 -0
- cursorflow/core/log_collector.py +410 -0
- cursorflow/core/log_monitor.py +179 -0
- cursorflow/core/persistent_session.py +910 -0
- cursorflow/core/report_generator.py +282 -0
- cursorflow/log_sources/local_file.py +198 -0
- cursorflow/log_sources/ssh_remote.py +210 -0
- cursorflow/updater.py +512 -0
- cursorflow-1.2.0.dist-info/METADATA +444 -0
- cursorflow-1.2.0.dist-info/RECORD +25 -0
- cursorflow-1.2.0.dist-info/WHEEL +5 -0
- cursorflow-1.2.0.dist-info/entry_points.txt +2 -0
- cursorflow-1.2.0.dist-info/top_level.txt +1 -0
@@ -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")
|