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,370 @@
1
+ """Test git status expandable section in file explorer."""
2
+
3
+ import os
4
+ import time
5
+ from pathlib import Path
6
+ from playwright.async_api import expect
7
+ from playwright.async_api import Locator
8
+ from testing_framework.core.base_test import BaseTest, TestResult, TestCategory
9
+
10
+ # Global test folder path
11
+ TESTING_FOLDER_PATH = "/home/menas/testing_folder"
12
+
13
+
14
+ class GitStatusUITest(BaseTest):
15
+ """Test the git status expandable section functionality in file explorer."""
16
+
17
+ def __init__(self):
18
+ super().__init__(
19
+ name="git_status_ui_test",
20
+ category=TestCategory.INTEGRATION,
21
+ description="Test git status expandable section in file explorer UI",
22
+ tags=["git", "ui", "file-explorer", "expandable"],
23
+ depends_on=["file_operations_test"],
24
+ start_url="/project/1d98e739-de00-4d65-a13b-c6c82173683f/"
25
+ )
26
+
27
+
28
+ async def run(self) -> TestResult:
29
+ """Test git status UI functionality with comprehensive UI interactions."""
30
+ page = self.playwright_manager.page
31
+ assert_that = self.assert_that()
32
+ stats = self.stats()
33
+
34
+
35
+ # Check if project loaded properly - look for files in explorer
36
+ file_items = page.locator(".file-item")
37
+ file_count = await file_items.count()
38
+ stats.record_stat("initial_files_in_explorer", file_count)
39
+
40
+ # If no files are loaded, wait longer and check for "No project loaded" message
41
+ if file_count == 0:
42
+ no_project_msg = page.locator("text=No project loaded")
43
+ no_project_count = await no_project_msg.count()
44
+ if no_project_count > 0:
45
+ await self.playwright_manager.take_screenshot("no_project_loaded_error")
46
+ # Don't fail immediately - the project might still be loaded (title shows testing_folder)
47
+ # Just record this as a potential UI issue and continue testing
48
+ stats.record_stat("file_explorer_shows_no_project", True)
49
+
50
+ # Wait additional time for slow loading
51
+ await page.wait_for_timeout(5000)
52
+ await self.playwright_manager.take_screenshot("after_additional_wait")
53
+ file_count = await file_items.count()
54
+
55
+ stats.record_stat("files_after_wait", file_count)
56
+
57
+ # Continue even if file explorer appears empty - the git section might still work
58
+ # The title bar shows "testing_folder" which suggests project is loaded
59
+
60
+ # PHASE 0: Test git diff viewer from file explorer
61
+ diff_options = [
62
+ '.context-menu-item:has-text("View Staged Changes")',
63
+ '.context-menu-item:has-text("View Unstaged Changes")',
64
+ '.context-menu-item:has-text("View All Changes")',
65
+ ]
66
+ new_file_item = page.locator('.file-item:has(.file-name:text("new_file1.py"))')
67
+ for option in diff_options:
68
+ await new_file_item.first.click(button='right')
69
+ await page.locator(option).hover()
70
+ await page.wait_for_timeout(500) # Wait for context menu to appear
71
+ await self.playwright_manager.take_screenshot(f"before_clicking_diff_option_{option.split(':')[1]}")
72
+ await page.locator(option).click()
73
+ await page.wait_for_timeout(1000)
74
+
75
+ staged_diff_tab = page.locator('.editor-tab:has-text("(head → staged)")')
76
+ unstaged_diff_tab = page.locator('.editor-tab:has-text("(staged → working)")')
77
+ all_diff_tab = page.locator('.editor-tab:has-text("(head → working)")')
78
+
79
+ assert_that.is_true(await staged_diff_tab.is_visible(), "Staged diff tab should be visible")
80
+ assert_that.is_true(await unstaged_diff_tab.is_visible(), "Unstaged diff tab should be visible")
81
+ assert_that.is_true(await all_diff_tab.is_visible(), "All changes diff tab should be visible")
82
+
83
+ # PHASE 1: Test git status section expansion/collapse
84
+ stats.start_timer("git_section_detection")
85
+
86
+ git_branch_section = page.locator(".git-branch-info")
87
+ git_section_count = await git_branch_section.count()
88
+ stats.record_stat("git_branch_sections_found", git_section_count)
89
+
90
+ if git_section_count == 0:
91
+ await self.playwright_manager.take_screenshot("no_git_section_found")
92
+ assert_that.is_true(False, "No git branch section found in file explorer")
93
+ return TestResult(self.name, False, "Git branch section not detected")
94
+
95
+ # Test initial collapsed state
96
+ is_expanded_initially = await git_branch_section.evaluate("el => el.classList.contains('expanded')")
97
+ assert_that.is_false(is_expanded_initially, "Git section should not be expanded initially")
98
+
99
+ # Test expansion
100
+ await git_branch_section.click()
101
+ await page.wait_for_timeout(500)
102
+ await self.playwright_manager.take_screenshot("git_section_expanded")
103
+
104
+ is_expanded_after_click = await git_branch_section.evaluate("el => el.classList.contains('expanded')")
105
+ assert_that.is_true(is_expanded_after_click, "Git section should be expanded after click")
106
+
107
+ # Check if detailed section is visible
108
+ git_detailed_section = page.locator(".git-detailed-section")
109
+ detailed_section_count = await git_detailed_section.count()
110
+ assert_that.is_true(detailed_section_count > 0, "Git detailed section should be visible when expanded")
111
+
112
+ detection_time = stats.end_timer("git_section_detection")
113
+ stats.record_stat("git_detection_time_ms", detection_time)
114
+
115
+ # PHASE 2: Test file type UI reflections and interactions
116
+ await self._test_file_modifications_ui(page, assert_that, stats)
117
+
118
+ # PHASE 3: Test staging/unstaging through UI
119
+ await self._test_staging_ui_interactions(page, assert_that, stats)
120
+
121
+ # PHASE 4: Test commit form UI
122
+ await self._test_commit_form_ui(page, assert_that, stats)
123
+
124
+ # PHASE 5: Test reverting files through UI
125
+ await self._test_revert_ui_interactions(page, assert_that, stats)
126
+
127
+ # PHASE 6: Test diff viewing through UI
128
+ await self._test_diff_ui_interactions(page, assert_that, stats)
129
+
130
+ # PHASE 7: Test group actions (stage all, unstage all, etc.)
131
+ await self._test_group_actions_ui(page, assert_that, stats)
132
+
133
+ # PHASE 8: Test collapse functionality
134
+ await git_branch_section.click()
135
+ await page.wait_for_timeout(500)
136
+ await self.playwright_manager.take_screenshot("git_section_collapsed")
137
+
138
+ is_collapsed = await git_branch_section.evaluate("el => !el.classList.contains('expanded')")
139
+ assert_that.is_true(is_collapsed, "Git section should collapse when clicked again")
140
+
141
+ detailed_section_after_collapse = await git_detailed_section.count()
142
+ assert_that.eq(detailed_section_after_collapse, 0, "Detailed section should be hidden when collapsed")
143
+
144
+ if assert_that.has_failures():
145
+ await self.playwright_manager.take_screenshot("test_failures")
146
+ return TestResult(self.name, False, assert_that.get_failure_message())
147
+
148
+ return TestResult(
149
+ self.name,
150
+ True,
151
+ f"Git status UI functionality tested successfully with comprehensive interactions",
152
+ artifacts=stats.get_stats()
153
+ )
154
+
155
+ async def _test_file_modifications_ui(self, page, assert_that, stats):
156
+ """Test that file modifications are properly reflected in the UI."""
157
+ await self.playwright_manager.take_screenshot("before_file_modifications")
158
+
159
+ # Look for existing files in the file explorer
160
+ file_items = page.locator(".file-item:not(.folder)")
161
+ file_count = await file_items.count()
162
+ stats.record_stat("initial_file_count", file_count)
163
+
164
+ if file_count > 0:
165
+ # Get the first file to test modifications
166
+ first_file = file_items.first
167
+ file_name = await first_file.locator(".file-name").text_content()
168
+ stats.record_stat("test_file_name", file_name)
169
+
170
+ # Look for git status indicators
171
+ git_indicators = page.locator(".git-status-indicator")
172
+ git_indicator_count = await git_indicators.count()
173
+ stats.record_stat("git_indicators_found", git_indicator_count)
174
+
175
+ # Check for different file status classes
176
+ modified_files = page.locator(".file-item.git-modified")
177
+ untracked_files = page.locator(".file-item.git-untracked")
178
+ added_files = page.locator(".file-item.git-added")
179
+
180
+ stats.record_stat("modified_files_count", await modified_files.count())
181
+ stats.record_stat("untracked_files_count", await untracked_files.count())
182
+ stats.record_stat("added_files_count", await added_files.count())
183
+
184
+ await self.playwright_manager.take_screenshot("file_status_indicators")
185
+
186
+ async def _test_staging_ui_interactions(self, page, assert_that, stats):
187
+ """Test staging and unstaging files through the UI."""
188
+ await self.playwright_manager.take_screenshot("before_staging_tests")
189
+
190
+ # Look for untracked files section
191
+ untracked_section = page.locator(".git-section-title").filter(has_text="Untracked Files")
192
+ untracked_count = await untracked_section.count()
193
+
194
+ if untracked_count > 0:
195
+ # Expand untracked files section
196
+ await untracked_section.click()
197
+ await page.wait_for_timeout(300)
198
+ await self.playwright_manager.take_screenshot("untracked_section_expanded")
199
+
200
+ # Look for stage buttons
201
+ stage_buttons = page.locator(".git-action-btn.stage")
202
+ stage_button_count = await stage_buttons.count()
203
+ stats.record_stat("stage_buttons_found", stage_button_count)
204
+
205
+ if stage_button_count > 0:
206
+ # Click first stage button
207
+ await stage_buttons.first.click()
208
+ await page.wait_for_timeout(1000) # Wait for staging operation
209
+ await self.playwright_manager.take_screenshot("after_staging_first_file")
210
+
211
+ # Check if staged changes section appeared
212
+ staged_section = page.locator(".git-section-title").filter(has_text="Staged Changes")
213
+ staged_section_count = await staged_section.count()
214
+ assert_that.is_true(staged_section_count > 0, "Staged changes section should appear after staging")
215
+
216
+ if staged_section_count > 0:
217
+ # Expand staged changes section
218
+ await staged_section.click()
219
+ await page.wait_for_timeout(300)
220
+ await self.playwright_manager.take_screenshot("staged_section_expanded")
221
+
222
+ # Look for unstage buttons
223
+ unstage_buttons = page.locator(".git-action-btn.unstage")
224
+ unstage_button_count = await unstage_buttons.count()
225
+ stats.record_stat("unstage_buttons_found", unstage_button_count)
226
+
227
+ if unstage_button_count > 0:
228
+ # Test unstaging
229
+ await unstage_buttons.first.click()
230
+ await page.wait_for_timeout(1000)
231
+ await self.playwright_manager.take_screenshot("after_unstaging_file")
232
+
233
+ async def _test_commit_form_ui(self, page, assert_that, stats):
234
+ """Test the commit form UI functionality."""
235
+ # First ensure we have staged files to enable commit form
236
+ stage_all_btn = page.locator(".git-group-btn.stage-all")
237
+ stage_all_count = await stage_all_btn.count()
238
+
239
+ if stage_all_count > 0:
240
+ await stage_all_btn.first.click()
241
+ await page.wait_for_timeout(1000)
242
+
243
+ await self.playwright_manager.take_screenshot("before_commit_form_test")
244
+
245
+ # Look for commit form
246
+ commit_form = page.locator(".git-commit-form")
247
+ commit_form_count = await commit_form.count()
248
+ stats.record_stat("commit_form_found", commit_form_count > 0)
249
+
250
+ if commit_form_count > 0:
251
+ # Test commit input field
252
+ commit_input = page.locator(".git-commit-input")
253
+ commit_input_count = await commit_input.count()
254
+ assert_that.is_true(commit_input_count > 0, "Commit input should be present")
255
+
256
+ # Test commit button
257
+ commit_btn = page.locator(".git-commit-btn").filter(has_text="Commit")
258
+ commit_btn_count = await commit_btn.count()
259
+ assert_that.is_true(commit_btn_count > 0, "Commit button should be present")
260
+
261
+ if commit_input_count > 0 and commit_btn_count > 0:
262
+ # Test that commit button is initially disabled
263
+ is_disabled = await commit_btn.is_disabled()
264
+ assert_that.is_true(is_disabled, "Commit button should be disabled initially")
265
+
266
+ # Enter commit message
267
+ test_message = "Test commit from UI automation"
268
+ await commit_input.fill(test_message)
269
+ await page.wait_for_timeout(300)
270
+ await self.playwright_manager.take_screenshot("commit_message_entered")
271
+
272
+ # Check that button is now enabled
273
+ is_disabled_after = await commit_btn.is_disabled()
274
+ assert_that.is_false(is_disabled_after, "Commit button should be enabled after entering message")
275
+
276
+ # Test cancel button
277
+ cancel_btn = page.locator(".git-commit-cancel")
278
+ cancel_count = await cancel_btn.count()
279
+ if cancel_count > 0:
280
+ await cancel_btn.click()
281
+ await page.wait_for_timeout(200)
282
+
283
+ # Check that message was cleared
284
+ input_value = await commit_input.input_value()
285
+ assert_that.eq(input_value, "", "Commit message should be cleared after cancel")
286
+ await self.playwright_manager.take_screenshot("commit_form_cancelled")
287
+
288
+ async def _test_revert_ui_interactions(self, page, assert_that, stats):
289
+ """Test reverting files through UI actions."""
290
+ await self.playwright_manager.take_screenshot("before_revert_tests")
291
+
292
+ # Look for unstaged changes section
293
+ unstaged_section = page.locator(".git-section-title").filter(has_text="Unstaged Changes")
294
+ unstaged_count = await unstaged_section.count()
295
+
296
+ if unstaged_count > 0:
297
+ # Expand unstaged section
298
+ await unstaged_section.click()
299
+ await page.wait_for_timeout(300)
300
+
301
+ # Look for revert buttons
302
+ revert_buttons = page.locator(".git-action-btn.revert")
303
+ revert_button_count = await revert_buttons.count()
304
+ stats.record_stat("revert_buttons_found", revert_button_count)
305
+
306
+ await self.playwright_manager.take_screenshot("revert_buttons_visible")
307
+
308
+ # Note: We won't actually click revert buttons in the test since that would
309
+ # permanently modify files and could break subsequent tests
310
+ # Instead we just verify they exist and are clickable
311
+ if revert_button_count > 0:
312
+ is_visible = await revert_buttons.first.is_visible()
313
+ assert_that.is_true(is_visible, "Revert button should be visible")
314
+
315
+ async def _test_diff_ui_interactions(self, page, assert_that, stats):
316
+ """Test diff viewing through UI buttons."""
317
+ await self.playwright_manager.take_screenshot("before_diff_tests")
318
+
319
+ # Look for diff buttons
320
+ diff_buttons = page.locator(".git-action-btn.diff")
321
+ diff_button_count = await diff_buttons.count()
322
+ stats.record_stat("diff_buttons_found", diff_button_count)
323
+
324
+ if diff_button_count > 0:
325
+ is_visible = await diff_buttons.first.is_visible()
326
+ assert_that.is_true(is_visible, "Diff button should be visible")
327
+
328
+ # Note: We could click diff buttons since they open tabs without modifying files
329
+ # But for now we just verify they exist
330
+ await self.playwright_manager.take_screenshot("diff_buttons_visible")
331
+
332
+ async def _test_group_actions_ui(self, page, assert_that, stats):
333
+ """Test group actions like stage all, unstage all, revert all."""
334
+ await self.playwright_manager.take_screenshot("before_group_actions_tests")
335
+
336
+ # Look for various group action buttons
337
+ stage_all_btns = page.locator(".git-group-btn.stage-all")
338
+ unstage_all_btns = page.locator(".git-group-btn.unstage-all")
339
+ revert_all_btns = page.locator(".git-group-btn.revert-all")
340
+ diff_all_btns = page.locator(".git-group-btn.diff-all")
341
+
342
+ stage_all_count = await stage_all_btns.count()
343
+ unstage_all_count = await unstage_all_btns.count()
344
+ revert_all_count = await revert_all_btns.count()
345
+ diff_all_count = await diff_all_btns.count()
346
+
347
+ stats.record_stat("stage_all_buttons", stage_all_count)
348
+ stats.record_stat("unstage_all_buttons", unstage_all_count)
349
+ stats.record_stat("revert_all_buttons", revert_all_count)
350
+ stats.record_stat("diff_all_buttons", diff_all_count)
351
+
352
+ # Test that at least some group action buttons exist
353
+ total_group_actions = stage_all_count + unstage_all_count + revert_all_count + diff_all_count
354
+ assert_that.is_true(total_group_actions > 0, "Should have at least some group action buttons")
355
+
356
+ await self.playwright_manager.take_screenshot("group_action_buttons_visible")
357
+
358
+ async def setup(self):
359
+ """Setup for git status UI test - this test depends on navigate_testing_folder_test for git setup."""
360
+ # This test relies on navigate_testing_folder_test to set up the git repository
361
+ # We don't need to do any additional setup here since that test creates
362
+ # the git repo and test files we need
363
+ pass
364
+
365
+
366
+ async def teardown(self):
367
+ """Teardown for git status UI test."""
368
+ # This test depends on navigate_testing_folder_test for setup
369
+ # We let that test handle cleanup as well
370
+ pass
@@ -0,0 +1,50 @@
1
+ """Login flow test - simplified and fast."""
2
+
3
+ import os
4
+
5
+ from testing_framework.core.base_test import BaseTest, TestResult, TestCategory
6
+
7
+
8
+ class LoginFlowTest(BaseTest):
9
+ """Test the basic login flow of the application."""
10
+
11
+ def __init__(self):
12
+ super().__init__(
13
+ name="login_flow_test",
14
+ category=TestCategory.SMOKE,
15
+ description="Verify that users can successfully log in to the application",
16
+ tags=["login", "authentication", "smoke"],
17
+ )
18
+
19
+ async def run(self) -> TestResult:
20
+ """Execute the login flow test."""
21
+ page = self.playwright_manager.page
22
+ assert_that = self.assert_that()
23
+
24
+ # Navigate to dashboard - should be accessible if logged in
25
+ base_url = '/'.join(page.url.split('/')[:3])
26
+ dashboard_url = f"{base_url}/dashboard/"
27
+ response = await page.goto(dashboard_url)
28
+
29
+ # Check if successfully reached dashboard
30
+ assert_that.status_ok(response, "Dashboard request")
31
+ assert_that.url_contains(page, "/dashboard", "Dashboard URL")
32
+
33
+ # Verify we have active sessions unless explicitly allowed to skip
34
+ allow_empty_sessions = os.getenv("ALLOW_EMPTY_SESSIONS", "false").lower() in ("1", "true", "yes")
35
+ if not allow_empty_sessions:
36
+ active_sessions = self.inspect().get_active_sessions()
37
+ assert_that.is_true(len(active_sessions) > 0, "Should have active sessions")
38
+
39
+ if assert_that.has_failures():
40
+ return TestResult(self.name, False, assert_that.get_failure_message())
41
+
42
+ return TestResult(self.name, True, f"Login successful. Dashboard at {page.url}")
43
+
44
+ async def setup(self):
45
+ """Setup for login test."""
46
+ pass
47
+
48
+ async def teardown(self):
49
+ """Teardown for login test."""
50
+ pass