import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from playwright.async_api import TimeoutError from browser_utils.page_controller_modules.chat import ChatController # Mock config constants CONSTANTS = { "CLEAR_CHAT_BUTTON_SELECTOR": "button.clear", "CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR": "button.confirm", "CLEAR_CHAT_VERIFY_TIMEOUT_MS": 1000, "CLICK_TIMEOUT_MS": 1000, "OVERLAY_SELECTOR": "div.overlay", "RESPONSE_CONTAINER_SELECTOR": "div.response", "SUBMIT_BUTTON_SELECTOR": "button.submit", "WAIT_FOR_ELEMENT_TIMEOUT_MS": 1000, } @pytest.fixture(autouse=True) def mock_constants(): with patch.multiple("browser_utils.page_controller_modules.chat", **CONSTANTS): yield @pytest.fixture def mock_page_controller(): controller = MagicMock() controller.page = MagicMock() controller.logger = MagicMock() controller.req_id = "test-req-id" # Setup page methods as AsyncMock controller.page.locator = MagicMock() controller.page.keyboard = MagicMock() controller.page.keyboard.press = AsyncMock() controller._check_disconnect = AsyncMock() return controller @pytest.fixture def chat_controller(mock_page_controller): # The BaseController __init__ requires (page, logger, req_id) # We'll just pass mock objects from mock_page_controller return ChatController( mock_page_controller.page, mock_page_controller.logger, mock_page_controller.req_id, ) @pytest.fixture def mock_expect_async(): with patch("browser_utils.page_controller_modules.chat.expect_async") as mock: # Create a mock object that supports .to_be_enabled(), .to_be_disabled(), etc. # These methods should return an awaitable (AsyncMock) assertion_mock = MagicMock() assertion_mock.to_be_enabled = AsyncMock() assertion_mock.to_be_disabled = AsyncMock() assertion_mock.to_be_visible = AsyncMock() assertion_mock.to_be_hidden = AsyncMock() mock.return_value = assertion_mock yield mock @pytest.fixture def mock_enable_temp_chat(): with patch( "browser_utils.page_controller_modules.chat.enable_temporary_chat_mode", new_callable=AsyncMock, ) as mock: yield mock @pytest.fixture def mock_save_snapshot(): with patch( "browser_utils.page_controller_modules.chat.save_error_snapshot", new_callable=AsyncMock, ) as mock: yield mock @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_clear_chat_history_success( chat_controller, mock_page_controller, mock_expect_async, mock_enable_temp_chat ): """Test successful chat clearing flow.""" mock_check_disconnect = MagicMock(return_value=False) # Setup locators submit_btn = MagicMock() submit_btn.click = AsyncMock() clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) response_container = MagicMock() # Mock locator calls def locator_side_effect(selector): if selector == CONSTANTS["SUBMIT_BUTTON_SELECTOR"]: return submit_btn elif selector == CONSTANTS["CLEAR_CHAT_BUTTON_SELECTOR"]: return clear_btn elif selector == CONSTANTS["CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR"]: return confirm_btn elif selector == CONSTANTS["OVERLAY_SELECTOR"]: return overlay elif selector == CONSTANTS["RESPONSE_CONTAINER_SELECTOR"]: return response_container return MagicMock() mock_page_controller.page.locator.side_effect = locator_side_effect # Mock response container .last response_container.last = response_container # Mock url mock_page_controller.page.url = "https://example.com/c/123" # Mock _execute_chat_clear and _verify_chat_cleared to simplify main flow test # (We will test them separately, but here we want to ensure they are called) with ( patch.object( chat_controller, "_execute_chat_clear", new_callable=AsyncMock ) as mock_exec, patch.object( chat_controller, "_verify_chat_cleared", new_callable=AsyncMock ) as mock_verify, ): await chat_controller.clear_chat_history(mock_check_disconnect) # Verify submit button flow assert submit_btn.click.called # Verify clear execution mock_exec.assert_awaited_once() mock_verify.assert_awaited_once() mock_enable_temp_chat.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_clear_chat_history_new_chat_skip( chat_controller, mock_page_controller, mock_expect_async ): """Test that clear chat is skipped if already on new_chat page.""" mock_check_disconnect = MagicMock(return_value=False) # Mock submit button check passes submit_btn = MagicMock() submit_btn.click = AsyncMock() mock_page_controller.page.locator.return_value = submit_btn # Mock clear button check fails (not enabled) mock_expect_async.return_value.to_be_enabled.side_effect = Exception("Not enabled") # Mock URL to be new_chat mock_page_controller.page.url = "https://example.com/prompts/new_chat" with patch.object( chat_controller, "_execute_chat_clear", new_callable=AsyncMock ) as mock_exec: await chat_controller.clear_chat_history(mock_check_disconnect) # Should catch exception, log info, and NOT call execute mock_exec.assert_not_called() # Verify we logged the skip message # We can't easily check log message content with MagicMock unless we configure it, # but we verify the flow didn't proceed. @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_overlay_visible( chat_controller, mock_page_controller ): """Test _execute_chat_clear when overlay is initially visible.""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=True) # Visible! # Setup expect_async mock for disappear check with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock() await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # Should click confirm directly confirm_btn.click.assert_awaited() clear_btn.click.assert_not_called() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_overlay_hidden_initially( chat_controller, mock_page_controller ): """Test _execute_chat_clear when overlay is initially hidden.""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) # Hidden initially with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() # Overlay appears mock_expect.return_value.to_be_hidden = AsyncMock() # Overlay disappears with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # Should click clear first clear_btn.click.assert_awaited() # Then check overlay visible mock_expect.return_value.to_be_visible.assert_awaited() # Then click confirm confirm_btn.click.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops(chat_controller, mock_page_controller): """Test _dismiss_backdrops logic.""" # Mock for survey iframe check (return 0 = no survey) survey_locator = MagicMock() survey_locator.count = AsyncMock(return_value=0) # Mock for backdrop - first call returns count 1 (exists), second call returns 0 (gone) backdrop = MagicMock() backdrop.count = AsyncMock(side_effect=[1, 0]) # Different locators for different selectors def locator_side_effect(selector): if "google-hats-survey" in selector or "google_hats" in selector: return survey_locator else: # cdk-overlay-backdrop return backdrop mock_page_controller.page.locator.side_effect = locator_side_effect mock_page_controller.page.evaluate = AsyncMock() with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock() await chat_controller._dismiss_backdrops() # Should have pressed Escape mock_page_controller.page.keyboard.press.assert_awaited_with("Escape") # Should have checked hidden mock_expect.return_value.to_be_hidden.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_verify_chat_cleared_success(chat_controller, mock_page_controller): """Test _verify_chat_cleared success.""" mock_check_disconnect = MagicMock(return_value=False) response_container = MagicMock() response_container.last = response_container mock_page_controller.page.locator.return_value = response_container with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock() await chat_controller._verify_chat_cleared(mock_check_disconnect) mock_expect.return_value.to_be_hidden.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_verify_chat_cleared_failure(chat_controller, mock_page_controller): """Test _verify_chat_cleared failure (should log warning but not raise).""" mock_check_disconnect = MagicMock(return_value=False) response_container = MagicMock() response_container.last = response_container mock_page_controller.page.locator.return_value = response_container with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=Exception("Still visible") ) # Should not raise exception await chat_controller._verify_chat_cleared(mock_check_disconnect) # Verify warning logged mock_page_controller.logger.warning.assert_called() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_retries(chat_controller, mock_page_controller): """Test _execute_chat_clear retries and force clicks.""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() # First click fails, second (force) succeeds clear_btn.click = AsyncMock(side_effect=[Exception("Click failed"), None]) clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() mock_expect.return_value.to_be_hidden = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ) as mock_dismiss: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # Verify retry logic assert clear_btn.click.call_count == 2 # Check second call had force=True call_args = clear_btn.click.call_args_list[1] assert call_args.kwargs.get("force") is True # Check dismiss backdrops called mock_dismiss.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_wait_disappear_timeout( chat_controller, mock_page_controller ): """Test _execute_chat_clear timeout waiting for dialog to disappear.""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=True) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # to_be_hidden always raises TimeoutError mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=TimeoutError("Timeout") ) with pytest.raises(Exception) as excinfo: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) assert ( "Maximum retries reached" in str(excinfo.value) or "maximum retries" in str(excinfo.value).lower() ) # ==================== New Tests for Improved Coverage ==================== @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_clear_chat_submit_button_cancelled_error( chat_controller, mock_page_controller, mock_expect_async ): """Test CancelledError handling during submit button check (lines 46-50).""" mock_check_disconnect = MagicMock(return_value=False) submit_btn = MagicMock() submit_btn.click = AsyncMock() mock_page_controller.page.locator.return_value = submit_btn # Submit button enabled check succeeds, but disabled check raises CancelledError mock_expect_async.return_value.to_be_enabled.side_effect = None mock_expect_async.return_value.to_be_disabled.side_effect = asyncio.CancelledError() with pytest.raises(asyncio.CancelledError): await chat_controller.clear_chat_history(mock_check_disconnect) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_clear_chat_button_not_enabled_non_new_chat( chat_controller, mock_page_controller, mock_expect_async ): """Test warning log when clear button not enabled on non-new_chat page (line 77).""" mock_check_disconnect = MagicMock(return_value=False) submit_btn = MagicMock() submit_btn.click = AsyncMock() mock_page_controller.page.locator.return_value = submit_btn # Clear button check fails mock_expect_async.return_value.to_be_enabled.side_effect = Exception("Not enabled") # URL is NOT new_chat mock_page_controller.page.url = "https://example.com/prompts/some_other_page" with patch.object( chat_controller, "_execute_chat_clear", new_callable=AsyncMock ) as mock_exec: await chat_controller.clear_chat_history(mock_check_disconnect) # Should NOT call execute mock_exec.assert_not_called() # Verify warning was logged (line 77) assert mock_page_controller.logger.warning.called @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_clear_chat_error_snapshot_non_disconnect( chat_controller, mock_page_controller, mock_save_snapshot ): """Test error snapshot saving for non-disconnect errors (lines 96-126).""" mock_check_disconnect = MagicMock(return_value=False) submit_btn = MagicMock() submit_btn.click = AsyncMock() clear_btn = MagicMock() confirm_btn = MagicMock() overlay = MagicMock() def locator_side_effect(selector): if "submit" in selector: return submit_btn elif "clear" in selector: return clear_btn elif "confirm" in selector: return confirm_btn else: return overlay mock_page_controller.page.locator.side_effect = locator_side_effect mock_page_controller.page.url = "https://example.com/c/123" # Simulate error in _execute_chat_clear test_error = ValueError("Test error") with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # Submit button check passes mock_expect.return_value.to_be_enabled = AsyncMock() mock_expect.return_value.to_be_disabled = AsyncMock( side_effect=Exception("ignore") ) with patch.object( chat_controller, "_execute_chat_clear", new_callable=AsyncMock ) as mock_exec: mock_exec.side_effect = test_error with patch( "browser_utils.page_controller_modules.chat.enable_temporary_chat_mode", new_callable=AsyncMock, ): try: await chat_controller.clear_chat_history(mock_check_disconnect) pytest.fail("Should have raised ValueError") except ValueError: pass # Verify save_error_snapshot was called (lines 111-125) mock_save_snapshot.assert_awaited_once() call_args = mock_save_snapshot.call_args assert "clear_chat_error_" in call_args[0][0] # Check error info is in extra_context dict extra_context = call_args.kwargs.get("extra_context", {}) assert extra_context.get("error_exception") == str(test_error) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_clear_chat_error_snapshot_disconnect_skip( chat_controller, mock_page_controller, mock_save_snapshot ): """Test that ClientDisconnectedError skips snapshot saving (lines 102-104).""" from models import ClientDisconnectedError mock_check_disconnect = MagicMock(return_value=False) submit_btn = MagicMock() submit_btn.click = AsyncMock() clear_btn = MagicMock() mock_page_controller.page.locator.return_value = clear_btn # Simulate ClientDisconnectedError disconnect_error = ClientDisconnectedError("Client gone") with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_enabled = AsyncMock() mock_expect.return_value.to_be_disabled = AsyncMock( side_effect=Exception("ignore") ) with patch.object( chat_controller, "_execute_chat_clear", new_callable=AsyncMock ) as mock_exec: mock_exec.side_effect = disconnect_error with patch( "browser_utils.page_controller_modules.chat.enable_temporary_chat_mode", new_callable=AsyncMock, ): try: await chat_controller.clear_chat_history(mock_check_disconnect) pytest.fail("Should have raised ClientDisconnectedError") except ClientDisconnectedError: pass # Verify save_error_snapshot was NOT called mock_save_snapshot.assert_not_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_overlay_timeout_check( chat_controller, mock_page_controller ): """Test TimeoutError handling in overlay visibility check (lines 141-143).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() # is_visible raises TimeoutError overlay.is_visible = AsyncMock(side_effect=TimeoutError("Timeout")) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() mock_expect.return_value.to_be_hidden = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # Should continue to clear button click path clear_btn.click.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_overlay_exception_check( chat_controller, mock_page_controller ): """Test generic Exception handling in overlay visibility check. Note: The implementation has a duplicate 'except Exception:' block, so the warning log is never reached. The exception is silently caught without logging. """ mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() # is_visible raises generic exception overlay.is_visible = AsyncMock(side_effect=ValueError("Some error")) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() mock_expect.return_value.to_be_hidden = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # The exception is caught silently and proceeds to clear button path clear_btn.click.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_dismiss_backdrops_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in first _dismiss_backdrops call (lines 164-165).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ) as mock_dismiss: mock_dismiss.side_effect = asyncio.CancelledError() with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_scroll_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in scroll_into_view_if_needed (lines 171-172).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() # scroll raises CancelledError clear_btn.scroll_into_view_if_needed = AsyncMock( side_effect=asyncio.CancelledError() ) confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch.object(chat_controller, "_dismiss_backdrops", new_callable=AsyncMock): with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_scroll_exception_pass( chat_controller, mock_page_controller ): """Test Exception in scroll_into_view_if_needed is caught (line 173-174).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() # scroll raises exception but should be caught clear_btn.scroll_into_view_if_needed = AsyncMock( side_effect=ValueError("Scroll error") ) confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() mock_expect.return_value.to_be_hidden = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # Should continue despite scroll error clear_btn.click.assert_awaited() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_first_click_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in first clear button click (line 176-177).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.scroll_into_view_if_needed = AsyncMock() # First click raises CancelledError clear_btn.click = AsyncMock(side_effect=asyncio.CancelledError()) confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch.object(chat_controller, "_dismiss_backdrops", new_callable=AsyncMock): with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_retry_dismiss_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in retry _dismiss_backdrops (lines 184-185).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.scroll_into_view_if_needed = AsyncMock() # First click fails clear_btn.click = AsyncMock(side_effect=ValueError("Click failed")) confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ) as mock_dismiss: # First call succeeds, second call raises CancelledError mock_dismiss.side_effect = [None, asyncio.CancelledError()] with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_force_click_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in force click (lines 192-193).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.scroll_into_view_if_needed = AsyncMock() # First click fails, force click raises CancelledError clear_btn.click = AsyncMock( side_effect=[ValueError("Click failed"), asyncio.CancelledError()] ) confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch.object(chat_controller, "_dismiss_backdrops", new_callable=AsyncMock): with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_force_click_failure( chat_controller, mock_page_controller, mock_save_snapshot ): """Test force click failure raises error (lines 194-196).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.scroll_into_view_if_needed = AsyncMock() # Both clicks fail clear_btn.click = AsyncMock( side_effect=[ValueError("Click failed"), ValueError("Force click failed")] ) confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch.object(chat_controller, "_dismiss_backdrops", new_callable=AsyncMock): with pytest.raises(ValueError) as excinfo: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) assert "Force click failed" in str(excinfo.value) # Verify error logged mock_page_controller.logger.error.assert_called() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_overlay_appear_timeout( chat_controller, mock_page_controller, mock_save_snapshot ): """Test overlay appear timeout saves snapshot (lines 207-211).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # Overlay appear timeout mock_expect.return_value.to_be_visible = AsyncMock( side_effect=TimeoutError("Timeout") ) with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): try: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) pytest.fail("Should have raised Exception") except Exception as e: assert "Timed out waiting for clear chat confirmation overlay" in str(e) # Verify snapshot saved mock_save_snapshot.assert_awaited_once() assert "clear_chat_overlay_timeout_" in mock_save_snapshot.call_args[0][0] @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_confirm_scroll_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in confirm button scroll (line 222).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() # Confirm scroll raises CancelledError confirm_btn.scroll_into_view_if_needed = AsyncMock( side_effect=asyncio.CancelledError() ) overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_confirm_click_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in confirm button click (lines 227-228).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() # Confirm click raises CancelledError confirm_btn.click = AsyncMock(side_effect=asyncio.CancelledError()) overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_confirm_force_click_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in confirm button force click (lines 237-238).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() # First click fails, force click raises CancelledError confirm_btn.click = AsyncMock( side_effect=[ValueError("Click failed"), asyncio.CancelledError()] ) overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_confirm_force_click_failure( chat_controller, mock_page_controller ): """Test confirm button force click failure (lines 239-243).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() clear_btn.click = AsyncMock() clear_btn.scroll_into_view_if_needed = AsyncMock() confirm_btn = MagicMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() # Both clicks fail confirm_btn.click = AsyncMock( side_effect=[ValueError("Click failed"), ValueError("Force click failed")] ) overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=False) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_visible = AsyncMock() with patch.object( chat_controller, "_dismiss_backdrops", new_callable=AsyncMock ): with pytest.raises(ValueError) as excinfo: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) assert "Force click failed" in str(excinfo.value) mock_page_controller.logger.error.assert_called() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_disappear_client_disconnected( chat_controller, mock_page_controller ): """Test ClientDisconnectedError during dialog disappear wait (lines 279-281).""" from models import ClientDisconnectedError mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=True) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # First to_be_hidden raises ClientDisconnectedError mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=ClientDisconnectedError("Client gone") ) try: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) pytest.fail("Should have raised ClientDisconnectedError") except ClientDisconnectedError: pass # Verify info log about disconnect mock_page_controller.logger.info.assert_any_call( "Client disconnected while waiting for clear confirmation dialog to disappear." ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_disappear_cancelled_error( chat_controller, mock_page_controller ): """Test CancelledError during dialog disappear wait (lines 282-284).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=True) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # First to_be_hidden raises CancelledError wrapped in other exception mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=asyncio.CancelledError() ) with pytest.raises(asyncio.CancelledError): await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_disappear_other_error_retry( chat_controller, mock_page_controller ): """Test other error during disappear with retry (lines 285-291).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=True) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # First 2 attempts raise ValueError, third succeeds mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=[ValueError("Error 1"), ValueError("Error 2"), None, None] ) await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) # Should have logged warnings assert mock_page_controller.logger.warning.call_count >= 2 @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_execute_chat_clear_disappear_other_error_max_retries( chat_controller, mock_page_controller ): """Test other error during disappear reaching max retries (line 290-291).""" mock_check_disconnect = MagicMock(return_value=False) clear_btn = MagicMock() confirm_btn = MagicMock() confirm_btn.click = AsyncMock() confirm_btn.scroll_into_view_if_needed = AsyncMock() overlay = MagicMock() overlay.is_visible = AsyncMock(return_value=True) with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: # All 3 attempts raise ValueError test_error = ValueError("Persistent error") mock_expect.return_value.to_be_hidden = AsyncMock(side_effect=test_error) try: await chat_controller._execute_chat_clear( clear_btn, confirm_btn, overlay, mock_check_disconnect ) pytest.fail("Should have raised ValueError") except ValueError as e: assert "Persistent error" in str(e) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_count_cancelled(chat_controller, mock_page_controller): """Test CancelledError in backdrop.count() (lines 308-309).""" backdrop = MagicMock() backdrop.count = AsyncMock(side_effect=asyncio.CancelledError()) mock_page_controller.page.locator.return_value = backdrop with pytest.raises(asyncio.CancelledError): await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_count_exception(chat_controller, mock_page_controller): """Test Exception in backdrop.count() sets cnt to 0 (lines 310-311).""" backdrop = MagicMock() # count raises exception backdrop.count = AsyncMock(side_effect=ValueError("Count error")) mock_page_controller.page.locator.return_value = backdrop # Should not raise, just continues with cnt=0 await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_keyboard_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in keyboard.press() (lines 324-325).""" backdrop = MagicMock() backdrop.count = AsyncMock(return_value=1) mock_page_controller.page.locator.return_value = backdrop mock_page_controller.page.keyboard.press = AsyncMock( side_effect=asyncio.CancelledError() ) with pytest.raises(asyncio.CancelledError): await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_expect_hidden_cancelled( chat_controller, mock_page_controller ): """Test CancelledError in expect backdrop hidden (lines 320-321).""" backdrop = MagicMock() backdrop.count = AsyncMock(return_value=1) mock_page_controller.page.locator.return_value = backdrop with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=asyncio.CancelledError() ) with pytest.raises(asyncio.CancelledError): await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_expect_hidden_exception( chat_controller, mock_page_controller ): """Test Exception in expect backdrop hidden is caught (lines 322-323).""" backdrop = MagicMock() # First call returns 1, second returns 0 backdrop.count = AsyncMock(side_effect=[1, 0]) mock_page_controller.page.locator.return_value = backdrop with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=ValueError("Hidden check error") ) # Should not raise await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_keyboard_exception( chat_controller, mock_page_controller ): """Test Exception in keyboard.press() is caught (lines 326-327).""" backdrop = MagicMock() backdrop.count = AsyncMock(side_effect=[1, 0]) mock_page_controller.page.locator.return_value = backdrop mock_page_controller.page.keyboard.press = AsyncMock( side_effect=ValueError("Keyboard error") ) # Should not raise await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_outer_cancelled(chat_controller, mock_page_controller): """Test CancelledError at outer try level (lines 330-331).""" # Locator itself raises CancelledError mock_page_controller.page.locator = MagicMock(side_effect=asyncio.CancelledError()) with pytest.raises(asyncio.CancelledError): await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_dismiss_backdrops_outer_exception(chat_controller, mock_page_controller): """Test Exception at outer try level is caught (lines 332-333).""" # Locator raises exception mock_page_controller.page.locator = MagicMock( side_effect=ValueError("Locator error") ) # Should not raise await chat_controller._dismiss_backdrops() @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_verify_chat_cleared_cancelled_error( chat_controller, mock_page_controller ): """Test CancelledError in _verify_chat_cleared (line 346-347).""" mock_check_disconnect = MagicMock(return_value=False) response_container = MagicMock() response_container.last = response_container mock_page_controller.page.locator.return_value = response_container with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock( side_effect=asyncio.CancelledError() ) with pytest.raises(asyncio.CancelledError): await chat_controller._verify_chat_cleared(mock_check_disconnect) # ==================== [Chat] Tag Logging Verification Tests ==================== @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_chat_tag_on_clear_start( chat_controller, mock_page_controller, mock_expect_async ): """Verify [Chat] tag used in logging when clearing chat starts.""" mock_check_disconnect = MagicMock(return_value=False) # Setup minimal mocks to trigger early return mock_expect_async.return_value.to_be_enabled.side_effect = Exception("Not enabled") mock_page_controller.page.url = "https://example.com/prompts/new_chat" await chat_controller.clear_chat_history(mock_check_disconnect) # Verify [Chat] tag was used (check debug calls) debug_calls = [ str(call) for call in mock_page_controller.logger.debug.call_args_list ] assert any("[Chat]" in str(call) for call in debug_calls) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_chat_tag_on_button_available( chat_controller, mock_page_controller, mock_expect_async, mock_enable_temp_chat ): """Verify [Chat] clear button available log is issued.""" mock_check_disconnect = MagicMock(return_value=False) submit_btn = MagicMock() submit_btn.click = AsyncMock() clear_btn = MagicMock() mock_page_controller.page.locator.return_value = clear_btn mock_page_controller.page.url = "https://example.com/c/123" with ( patch.object(chat_controller, "_execute_chat_clear", new_callable=AsyncMock), patch.object(chat_controller, "_verify_chat_cleared", new_callable=AsyncMock), ): await chat_controller.clear_chat_history(mock_check_disconnect) # Verify [Chat] clear button available log debug_calls = [ str(call) for call in mock_page_controller.logger.debug.call_args_list ] assert any( "clear button available" in str(call).lower() or "[Chat]" in str(call) for call in debug_calls ) @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_chat_tag_on_verify_success(chat_controller, mock_page_controller): """Verify [Chat] verification passed log on successful verification.""" mock_check_disconnect = MagicMock(return_value=False) response_container = MagicMock() response_container.last = response_container mock_page_controller.page.locator.return_value = response_container with patch( "browser_utils.page_controller_modules.chat.expect_async" ) as mock_expect: mock_expect.return_value.to_be_hidden = AsyncMock() await chat_controller._verify_chat_cleared(mock_check_disconnect) # Verify [Chat] verification passed log debug_calls = [ str(call) for call in mock_page_controller.logger.debug.call_args_list ] assert any( "verification passed" in str(call).lower() or "[Chat]" in str(call) for call in debug_calls )