portacode 0.3.4.dev0__py3-none-any.whl โ†’ 1.4.11.dev0__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.

Potentially problematic release.


This version of portacode might be problematic. Click here for more details.

Files changed (93) hide show
  1. portacode/_version.py +16 -3
  2. portacode/cli.py +155 -19
  3. portacode/connection/client.py +152 -12
  4. portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +1577 -0
  5. portacode/connection/handlers/__init__.py +43 -1
  6. portacode/connection/handlers/base.py +122 -18
  7. portacode/connection/handlers/chunked_content.py +244 -0
  8. portacode/connection/handlers/diff_handlers.py +603 -0
  9. portacode/connection/handlers/file_handlers.py +902 -17
  10. portacode/connection/handlers/project_aware_file_handlers.py +226 -0
  11. portacode/connection/handlers/project_state/README.md +312 -0
  12. portacode/connection/handlers/project_state/__init__.py +92 -0
  13. portacode/connection/handlers/project_state/file_system_watcher.py +179 -0
  14. portacode/connection/handlers/project_state/git_manager.py +1502 -0
  15. portacode/connection/handlers/project_state/handlers.py +875 -0
  16. portacode/connection/handlers/project_state/manager.py +1331 -0
  17. portacode/connection/handlers/project_state/models.py +108 -0
  18. portacode/connection/handlers/project_state/utils.py +50 -0
  19. portacode/connection/handlers/project_state_handlers.py +45 -0
  20. portacode/connection/handlers/proxmox_infra.py +307 -0
  21. portacode/connection/handlers/registry.py +53 -10
  22. portacode/connection/handlers/session.py +705 -53
  23. portacode/connection/handlers/system_handlers.py +142 -8
  24. portacode/connection/handlers/tab_factory.py +389 -0
  25. portacode/connection/handlers/terminal_handlers.py +150 -11
  26. portacode/connection/handlers/update_handler.py +61 -0
  27. portacode/connection/multiplex.py +60 -2
  28. portacode/connection/terminal.py +695 -28
  29. portacode/keypair.py +63 -1
  30. portacode/link_capture/__init__.py +38 -0
  31. portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  32. portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  33. portacode/link_capture/bin/elinks +3 -0
  34. portacode/link_capture/bin/gio-open +3 -0
  35. portacode/link_capture/bin/gnome-open +3 -0
  36. portacode/link_capture/bin/gvfs-open +3 -0
  37. portacode/link_capture/bin/kde-open +3 -0
  38. portacode/link_capture/bin/kfmclient +3 -0
  39. portacode/link_capture/bin/link_capture_exec.sh +11 -0
  40. portacode/link_capture/bin/link_capture_wrapper.py +75 -0
  41. portacode/link_capture/bin/links +3 -0
  42. portacode/link_capture/bin/links2 +3 -0
  43. portacode/link_capture/bin/lynx +3 -0
  44. portacode/link_capture/bin/mate-open +3 -0
  45. portacode/link_capture/bin/netsurf +3 -0
  46. portacode/link_capture/bin/sensible-browser +3 -0
  47. portacode/link_capture/bin/w3m +3 -0
  48. portacode/link_capture/bin/x-www-browser +3 -0
  49. portacode/link_capture/bin/xdg-open +3 -0
  50. portacode/logging_categories.py +140 -0
  51. portacode/pairing.py +103 -0
  52. portacode/service.py +6 -0
  53. portacode/static/js/test-ntp-clock.html +63 -0
  54. portacode/static/js/utils/ntp-clock.js +232 -0
  55. portacode/utils/NTP_ARCHITECTURE.md +136 -0
  56. portacode/utils/__init__.py +1 -0
  57. portacode/utils/diff_apply.py +456 -0
  58. portacode/utils/diff_renderer.py +371 -0
  59. portacode/utils/ntp_clock.py +65 -0
  60. portacode-1.4.11.dev0.dist-info/METADATA +298 -0
  61. portacode-1.4.11.dev0.dist-info/RECORD +97 -0
  62. {portacode-0.3.4.dev0.dist-info โ†’ portacode-1.4.11.dev0.dist-info}/WHEEL +1 -1
  63. portacode-1.4.11.dev0.dist-info/top_level.txt +3 -0
  64. test_modules/README.md +296 -0
  65. test_modules/__init__.py +1 -0
  66. test_modules/test_device_online.py +44 -0
  67. test_modules/test_file_operations.py +743 -0
  68. test_modules/test_git_status_ui.py +370 -0
  69. test_modules/test_login_flow.py +50 -0
  70. test_modules/test_navigate_testing_folder.py +361 -0
  71. test_modules/test_play_store_screenshots.py +294 -0
  72. test_modules/test_terminal_buffer_performance.py +261 -0
  73. test_modules/test_terminal_interaction.py +80 -0
  74. test_modules/test_terminal_loading_race_condition.py +95 -0
  75. test_modules/test_terminal_start.py +56 -0
  76. testing_framework/.env.example +21 -0
  77. testing_framework/README.md +334 -0
  78. testing_framework/__init__.py +17 -0
  79. testing_framework/cli.py +326 -0
  80. testing_framework/core/__init__.py +1 -0
  81. testing_framework/core/base_test.py +336 -0
  82. testing_framework/core/cli_manager.py +177 -0
  83. testing_framework/core/hierarchical_runner.py +577 -0
  84. testing_framework/core/playwright_manager.py +520 -0
  85. testing_framework/core/runner.py +447 -0
  86. testing_framework/core/shared_cli_manager.py +234 -0
  87. testing_framework/core/test_discovery.py +112 -0
  88. testing_framework/requirements.txt +12 -0
  89. portacode-0.3.4.dev0.dist-info/METADATA +0 -236
  90. portacode-0.3.4.dev0.dist-info/RECORD +0 -27
  91. portacode-0.3.4.dev0.dist-info/top_level.txt +0 -1
  92. {portacode-0.3.4.dev0.dist-info โ†’ portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
  93. {portacode-0.3.4.dev0.dist-info โ†’ portacode-1.4.11.dev0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,520 @@
1
+ """Playwright session management with comprehensive recording and logging."""
2
+
3
+ import os
4
+ import asyncio
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any, List
7
+ import logging
8
+ import json
9
+ import time
10
+ from datetime import datetime
11
+ from urllib.parse import urlparse
12
+
13
+ try:
14
+ from playwright.async_api import async_playwright, Browser, BrowserContext, Page
15
+ PLAYWRIGHT_AVAILABLE = True
16
+ except ImportError:
17
+ PLAYWRIGHT_AVAILABLE = False
18
+
19
+
20
+ class PlaywrightManager:
21
+ """Manages Playwright sessions with comprehensive recording and logging."""
22
+
23
+ def __init__(self, test_name: str, recordings_dir: str = "test_recordings"):
24
+ if not PLAYWRIGHT_AVAILABLE:
25
+ raise ImportError("Playwright is not installed. Run: pip install playwright")
26
+
27
+ self.test_name = test_name
28
+ self.recordings_dir = Path(recordings_dir)
29
+ self.recordings_dir.mkdir(exist_ok=True)
30
+
31
+ # Create subdirectory for this test
32
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
33
+ self.test_recordings_dir = self.recordings_dir / f"{test_name}_{timestamp}"
34
+ self.test_recordings_dir.mkdir(exist_ok=True)
35
+
36
+ self.playwright = None
37
+ self.browser: Optional[Browser] = None
38
+ self.context: Optional[BrowserContext] = None
39
+ self.page: Optional[Page] = None
40
+
41
+ self.logger = logging.getLogger(f"playwright_manager.{test_name}")
42
+ self.logger.setLevel(logging.WARNING) # Only show warnings and errors
43
+
44
+ # Recording and logging paths
45
+ self.video_path = self.test_recordings_dir / "recording.webm"
46
+ self.screenshot_dir = self.test_recordings_dir / "screenshots"
47
+ self.screenshot_dir.mkdir(exist_ok=True)
48
+ self.trace_path = self.test_recordings_dir / "trace.zip"
49
+ self.har_path = self.test_recordings_dir / "network.har"
50
+ self.console_log_path = self.test_recordings_dir / "console.log"
51
+ self.actions_log_path = self.test_recordings_dir / "actions.json"
52
+ self.websocket_log_path = self.test_recordings_dir / "websockets.json"
53
+
54
+ # Action tracking
55
+ self.actions_log: List[Dict[str, Any]] = []
56
+ self.screenshot_counter = 0
57
+ self.websocket_logs: List[Dict[str, Any]] = []
58
+
59
+ async def start_session(self,
60
+ url: Optional[str] = None,
61
+ username: Optional[str] = None,
62
+ password: Optional[str] = None,
63
+ browser_type: Optional[str] = None,
64
+ headless: Optional[bool] = None) -> bool:
65
+ """Start Playwright session with comprehensive recording."""
66
+ try:
67
+ # Load environment variables
68
+ env_url = os.getenv('TEST_BASE_URL', 'http://192.168.1.188:8001/')
69
+ env_username = os.getenv('TEST_USERNAME')
70
+ env_password = os.getenv('TEST_PASSWORD')
71
+ env_browser = os.getenv('TEST_BROWSER', 'chromium')
72
+ env_headless = os.getenv('TEST_HEADLESS', 'false').lower() in ('true', '1', 'yes')
73
+ env_video_width = int(os.getenv('TEST_VIDEO_WIDTH', '1920'))
74
+ env_video_height = int(os.getenv('TEST_VIDEO_HEIGHT', '1080'))
75
+ env_viewport_width = os.getenv('TEST_VIEWPORT_WIDTH')
76
+ env_viewport_height = os.getenv('TEST_VIEWPORT_HEIGHT')
77
+ env_device_scale = os.getenv('TEST_DEVICE_SCALE_FACTOR')
78
+ env_is_mobile = os.getenv('TEST_IS_MOBILE')
79
+ env_has_touch = os.getenv('TEST_HAS_TOUCH')
80
+ env_user_agent = os.getenv('TEST_USER_AGENT')
81
+ automation_token = os.getenv('TEST_RUNNER_BYPASS_TOKEN')
82
+
83
+ # Use provided values or fall back to environment
84
+ self.base_url = url or env_url
85
+ self.username = username or env_username
86
+ self.password = password or env_password
87
+ browser_type = browser_type or env_browser
88
+ headless = headless if headless is not None else env_headless
89
+
90
+ if not self.username or not self.password:
91
+ self.logger.error("Username and password must be provided via parameters or environment variables")
92
+ return False
93
+
94
+ self.logger.info(f"Starting Playwright session for test: {self.test_name}")
95
+ self.logger.info(f"Target URL: {self.base_url}")
96
+ self.logger.info(f"Browser: {browser_type}, Headless: {headless}")
97
+
98
+ # Start Playwright
99
+ try:
100
+ self.playwright = await async_playwright().start()
101
+ self.logger.info("Playwright started successfully")
102
+ except Exception as e:
103
+ raise Exception(f"Failed to start Playwright: {e}")
104
+
105
+ # Launch browser with optimized settings for video recording
106
+ try:
107
+ # Common args for better video recording quality
108
+ launch_args = [
109
+ '--disable-blink-features=AutomationControlled',
110
+ '--disable-dev-shm-usage',
111
+ '--disable-gpu' if headless else '--force-gpu-mem-available-mb=2048',
112
+ '--no-sandbox',
113
+ '--disable-background-timer-throttling',
114
+ '--disable-backgrounding-occluded-windows',
115
+ '--disable-renderer-backgrounding'
116
+ ]
117
+
118
+ if browser_type == "firefox":
119
+ self.browser = await self.playwright.firefox.launch(
120
+ headless=headless,
121
+ args=launch_args[:3] # Firefox doesn't support all Chromium args
122
+ )
123
+ elif browser_type == "webkit":
124
+ self.browser = await self.playwright.webkit.launch(headless=headless)
125
+ else:
126
+ self.browser = await self.playwright.chromium.launch(
127
+ headless=headless,
128
+ args=launch_args
129
+ )
130
+ self.logger.info(f"Browser ({browser_type}) launched successfully with optimized recording settings")
131
+ except Exception as e:
132
+ raise Exception(f"Failed to launch {browser_type} browser: {e}")
133
+
134
+ # Create context with recording enabled and proper viewport
135
+ viewport_size = {
136
+ "width": int(env_viewport_width) if env_viewport_width else env_video_width,
137
+ "height": int(env_viewport_height) if env_viewport_height else env_video_height
138
+ }
139
+ video_size = {"width": env_video_width, "height": env_video_height}
140
+ context_kwargs = {
141
+ "record_video_dir": str(self.test_recordings_dir),
142
+ "record_video_size": video_size,
143
+ "record_har_path": str(self.har_path),
144
+ "record_har_omit_content": False,
145
+ "viewport": viewport_size
146
+ }
147
+
148
+ if env_device_scale:
149
+ try:
150
+ context_kwargs["device_scale_factor"] = float(env_device_scale)
151
+ except ValueError:
152
+ self.logger.warning(f"Invalid TEST_DEVICE_SCALE_FACTOR '{env_device_scale}' - ignoring")
153
+
154
+ if env_is_mobile:
155
+ context_kwargs["is_mobile"] = env_is_mobile.lower() in ('true', '1', 'yes')
156
+
157
+ if env_has_touch:
158
+ context_kwargs["has_touch"] = env_has_touch.lower() in ('true', '1', 'yes')
159
+
160
+ if env_user_agent:
161
+ context_kwargs["user_agent"] = env_user_agent
162
+
163
+ self.context = await self.browser.new_context(**context_kwargs)
164
+ self.logger.info(
165
+ "Viewport configured: %sx%s (device scale: %s, mobile: %s, touch: %s)",
166
+ viewport_size["width"],
167
+ viewport_size["height"],
168
+ context_kwargs.get("device_scale_factor", 1.0),
169
+ context_kwargs.get("is_mobile", False),
170
+ context_kwargs.get("has_touch", False),
171
+ )
172
+ if automation_token:
173
+ parsed_base = urlparse(self.base_url)
174
+ target_host = parsed_base.hostname
175
+ target_scheme = parsed_base.scheme or "http"
176
+ header_name = "X-Portacode-Automation"
177
+
178
+ async def automation_header_route(route, request):
179
+ headers = dict(request.headers)
180
+ parsed_request = urlparse(request.url)
181
+ if parsed_request.hostname == target_host and parsed_request.scheme == target_scheme:
182
+ headers[header_name] = automation_token
183
+ else:
184
+ headers.pop(header_name, None)
185
+ await route.continue_(headers=headers)
186
+
187
+ await self.context.route("**/*", automation_header_route)
188
+ self.logger.info("Automation bypass header restricted to same-origin requests")
189
+
190
+ self.logger.info(f"Video recording configured: {env_video_width}x{env_video_height}")
191
+
192
+ # Start tracing
193
+ await self.context.tracing.start(
194
+ screenshots=True,
195
+ snapshots=True,
196
+ sources=True
197
+ )
198
+
199
+ # Create page
200
+ self.page = await self.context.new_page()
201
+
202
+ # Set up console logging
203
+ self.console_logs = []
204
+ self.page.on("console", self._handle_console_message)
205
+
206
+ # Set up WebSocket logging
207
+ self.page.on("websocket", self._handle_websocket)
208
+
209
+ # Set up request/response logging
210
+ self.page.on("request", self._handle_request)
211
+ self.page.on("response", self._handle_response)
212
+
213
+ # Navigate to base URL
214
+ await self.log_action("navigate", {"url": self.base_url})
215
+ await self.page.goto(self.base_url)
216
+ await self.take_screenshot("initial_load")
217
+
218
+ # Perform login if credentials provided
219
+ if self.username and self.password:
220
+ await self._perform_login()
221
+
222
+ self.logger.info("Playwright session started successfully")
223
+ return True
224
+
225
+ except Exception as e:
226
+ error_msg = f"Failed to start Playwright session: {e}"
227
+ self.logger.error(error_msg)
228
+ await self.cleanup()
229
+ return False
230
+
231
+ async def _perform_login(self):
232
+ """Perform login using provided credentials."""
233
+ try:
234
+ # Navigate to login page first
235
+ login_url = f"{self.base_url}accounts/login/"
236
+ await self.page.goto(login_url)
237
+ await self.log_action("navigate_to_login", {"url": login_url})
238
+ await self.take_screenshot("login_page")
239
+
240
+ await self.log_action("login_start", {"username": self.username})
241
+
242
+ # Look for common login form elements
243
+ username_selectors = [
244
+ 'input[name="username"]',
245
+ 'input[name="email"]',
246
+ 'input[type="email"]',
247
+ 'input[id="username"]',
248
+ 'input[id="email"]',
249
+ '#id_username',
250
+ '#id_email'
251
+ ]
252
+
253
+ password_selectors = [
254
+ 'input[name="password"]',
255
+ 'input[type="password"]',
256
+ 'input[id="password"]',
257
+ '#id_password'
258
+ ]
259
+
260
+ submit_selectors = [
261
+ 'button[type="submit"]',
262
+ 'input[type="submit"]',
263
+ 'button:has-text("Login")',
264
+ 'button:has-text("Sign In")',
265
+ '.btn-primary',
266
+ '#login-button'
267
+ ]
268
+
269
+ # Find and fill username
270
+ username_filled = False
271
+ for selector in username_selectors:
272
+ try:
273
+ if await self.page.is_visible(selector):
274
+ await self.page.fill(selector, self.username)
275
+ await self.log_action("fill_username", {"selector": selector})
276
+ username_filled = True
277
+ break
278
+ except:
279
+ continue
280
+
281
+ if not username_filled:
282
+ raise Exception("Could not find username input field")
283
+
284
+ # Find and fill password
285
+ password_filled = False
286
+ for selector in password_selectors:
287
+ try:
288
+ if await self.page.is_visible(selector):
289
+ await self.page.fill(selector, self.password)
290
+ await self.log_action("fill_password", {"selector": selector})
291
+ password_filled = True
292
+ break
293
+ except:
294
+ continue
295
+
296
+ if not password_filled:
297
+ raise Exception("Could not find password input field")
298
+
299
+ await self.take_screenshot("login_form_filled")
300
+
301
+ # Submit form
302
+ submitted = False
303
+ for selector in submit_selectors:
304
+ try:
305
+ if await self.page.is_visible(selector):
306
+ await self.page.click(selector)
307
+ await self.log_action("click_submit", {"selector": selector})
308
+ submitted = True
309
+ break
310
+ except:
311
+ continue
312
+
313
+ if not submitted:
314
+ # Try pressing Enter on password field
315
+ for selector in password_selectors:
316
+ try:
317
+ await self.page.press(selector, "Enter")
318
+ await self.log_action("press_enter", {"selector": selector})
319
+ submitted = True
320
+ break
321
+ except:
322
+ continue
323
+
324
+ if not submitted:
325
+ raise Exception("Could not submit login form")
326
+
327
+ # Wait for navigation or login success
328
+ await self.page.wait_for_load_state("networkidle")
329
+ await self.take_screenshot("post_login")
330
+
331
+ await self.log_action("login_complete", {"success": True})
332
+ self.logger.info("Login completed successfully")
333
+
334
+ except Exception as e:
335
+ await self.log_action("login_error", {"error": str(e)})
336
+ self.logger.error(f"Login failed: {e}")
337
+ await self.take_screenshot("login_error")
338
+ raise
339
+
340
+ async def take_screenshot(self, name: str) -> Path:
341
+ """Take a screenshot with automatic naming."""
342
+ if not self.page:
343
+ raise RuntimeError("No active page for screenshot")
344
+
345
+ self.screenshot_counter += 1
346
+ screenshot_path = self.screenshot_dir / f"{self.screenshot_counter:03d}_{name}.png"
347
+
348
+ await self.page.screenshot(path=str(screenshot_path))
349
+ await self.log_action("screenshot", {
350
+ "name": name,
351
+ "path": str(screenshot_path),
352
+ "counter": self.screenshot_counter
353
+ })
354
+
355
+ self.logger.info(f"Screenshot saved: {screenshot_path}")
356
+ return screenshot_path
357
+
358
+ async def log_action(self, action_type: str, details: Dict[str, Any]):
359
+ """Log an action with timestamp and details."""
360
+ action_entry = {
361
+ "timestamp": datetime.now().isoformat(),
362
+ "action_type": action_type,
363
+ "details": details
364
+ }
365
+
366
+ self.actions_log.append(action_entry)
367
+
368
+ # Write to actions log file
369
+ try:
370
+ with open(self.actions_log_path, 'w') as f:
371
+ json.dump(self.actions_log, f, indent=2)
372
+ except Exception as e:
373
+ self.logger.error(f"Failed to write actions log: {e}")
374
+
375
+ async def log_timeline_marker(self, phase: str, description: str = ""):
376
+ """Log a timeline marker for better test debugging and trace correlation."""
377
+ timestamp = datetime.now().isoformat()
378
+ marker_details = {
379
+ "phase": phase,
380
+ "description": description,
381
+ "timestamp": timestamp
382
+ }
383
+
384
+ # Log to actions for timeline tracking
385
+ await self.log_action("TIMELINE_MARKER", marker_details)
386
+
387
+ # Also log to console for visibility in trace viewer
388
+ if self.page:
389
+ try:
390
+ # Inject a console log into the page that will show up in traces
391
+ script = f"""
392
+ console.log('๐Ÿงช TEST PHASE: {phase}' + ({repr(description)} ? ' - ' + {repr(description)} : ''));
393
+ """
394
+ asyncio.create_task(self.page.evaluate(script))
395
+ except Exception as e:
396
+ self.logger.warning(f"Could not inject timeline marker into page: {e}")
397
+
398
+ def _handle_console_message(self, msg):
399
+ """Handle console messages from the page."""
400
+ console_entry = {
401
+ "timestamp": datetime.now().isoformat(),
402
+ "type": msg.type,
403
+ "text": msg.text
404
+ }
405
+ self.console_logs.append(console_entry)
406
+
407
+ # Write to console log file
408
+ try:
409
+ with open(self.console_log_path, 'w') as f:
410
+ json.dump(self.console_logs, f, indent=2)
411
+ except:
412
+ pass
413
+
414
+ def _handle_websocket(self, websocket):
415
+ """Handle WebSocket connections."""
416
+ self.logger.info(f"WebSocket opened: {websocket.url}")
417
+ websocket.on("framesent", lambda payload: self._log_websocket_message("sent", websocket.url, payload))
418
+ websocket.on("framereceived", lambda payload: self._log_websocket_message("received", websocket.url, payload))
419
+ websocket.on("close", lambda: self.logger.info(f"WebSocket closed: {websocket.url}"))
420
+
421
+ def _log_websocket_message(self, direction: str, url: str, payload: Any):
422
+ """Log a WebSocket message."""
423
+ try:
424
+ parsed_payload = json.loads(payload)
425
+ except (json.JSONDecodeError, TypeError):
426
+ parsed_payload = payload
427
+
428
+ message_entry = {
429
+ "timestamp": datetime.now().isoformat(),
430
+ "direction": direction,
431
+ "url": url,
432
+ "payload": parsed_payload
433
+ }
434
+ self.websocket_logs.append(message_entry)
435
+
436
+ try:
437
+ with open(self.websocket_log_path, 'w') as f:
438
+ json.dump(self.websocket_logs, f, indent=2)
439
+ except Exception as e:
440
+ self.logger.error(f"Failed to write websocket log: {e}")
441
+
442
+ def _handle_request(self, request):
443
+ """Handle network requests."""
444
+ self.logger.debug(f"Request: {request.method} {request.url}")
445
+
446
+ def _handle_response(self, response):
447
+ """Handle network responses."""
448
+ self.logger.debug(f"Response: {response.status} {response.url}")
449
+
450
+ async def cleanup(self):
451
+ """Clean up Playwright resources and finalize recordings."""
452
+ try:
453
+ if self.context:
454
+ # Stop tracing
455
+ await self.context.tracing.stop(path=str(self.trace_path))
456
+
457
+ if self.page:
458
+ await self.page.close()
459
+
460
+ if self.context:
461
+ await self.context.close()
462
+
463
+ if self.browser:
464
+ await self.browser.close()
465
+
466
+ if self.playwright:
467
+ await self.playwright.stop()
468
+
469
+ # Generate summary report
470
+ await self._generate_summary_report()
471
+
472
+ self.logger.info(f"Playwright session cleaned up. Recordings saved to: {self.test_recordings_dir}")
473
+
474
+ except Exception as e:
475
+ self.logger.error(f"Error during cleanup: {e}")
476
+
477
+ async def _generate_summary_report(self):
478
+ """Generate a summary report of the test session."""
479
+ try:
480
+ summary = {
481
+ "test_name": self.test_name,
482
+ "start_time": self.actions_log[0]["timestamp"] if self.actions_log else None,
483
+ "end_time": datetime.now().isoformat(),
484
+ "total_actions": len(self.actions_log),
485
+ "total_screenshots": self.screenshot_counter,
486
+ "total_console_logs": len(getattr(self, 'console_logs', [])),
487
+ "total_websocket_logs": len(self.websocket_logs),
488
+ "recordings": {
489
+ "video": str(self.video_path) if self.video_path.exists() else None,
490
+ "trace": str(self.trace_path) if self.trace_path.exists() else None,
491
+ "har": str(self.har_path) if self.har_path.exists() else None,
492
+ "screenshots_dir": str(self.screenshot_dir),
493
+ "console_log": str(self.console_log_path),
494
+ "actions_log": str(self.actions_log_path),
495
+ "websocket_log": str(self.websocket_log_path)
496
+ }
497
+ }
498
+
499
+ summary_path = self.test_recordings_dir / "summary.json"
500
+ with open(summary_path, 'w') as f:
501
+ json.dump(summary, f, indent=2)
502
+
503
+ self.logger.info(f"Summary report generated: {summary_path}")
504
+
505
+ except Exception as e:
506
+ self.logger.error(f"Failed to generate summary report: {e}")
507
+
508
+ def get_recordings_info(self) -> Dict[str, Any]:
509
+ """Get information about all recordings for this test."""
510
+ return {
511
+ "test_name": self.test_name,
512
+ "recordings_dir": str(self.test_recordings_dir),
513
+ "video_path": str(self.video_path) if self.video_path.exists() else None,
514
+ "trace_path": str(self.trace_path) if self.trace_path.exists() else None,
515
+ "har_path": str(self.har_path) if self.har_path.exists() else None,
516
+ "screenshot_count": self.screenshot_counter,
517
+ "actions_count": len(self.actions_log),
518
+ "console_logs_count": len(getattr(self, 'console_logs', [])),
519
+ "websocket_logs_count": len(self.websocket_logs)
520
+ }