peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
47.9 kB
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
)