Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
"""Whole-field clearing must not depend on text length or keyboard shortcuts."""
|
|
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from artemis.controllers.unified_controller import UnifiedMobileController
|
|
from artemis.drivers.android.adb_driver import AndroidAdbDriver
|
|
from artemis.drivers.android.input_ime import AndroidInputIME
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
["ARTEMIS_PRO_R3_20261026", "长文本🙂\n" * 80, ""],
|
|
ids=["23-chars", "long-unicode-multiline", "empty"],
|
|
)
|
|
async def test_full_clear_replaces_the_whole_field(value):
|
|
field = {"text": value}
|
|
device, ui = Mock(), Mock()
|
|
|
|
def shell(command, **kwargs):
|
|
# A field that ignores Ctrl+A: the previous implementation leaves AR.
|
|
if "67" in command:
|
|
field["text"] = field["text"][: -command.count("67")]
|
|
return ""
|
|
|
|
def clear():
|
|
field["text"] = ""
|
|
return True
|
|
|
|
device.shell.side_effect = shell
|
|
ui.clear_text.side_effect = clear
|
|
driver = AndroidAdbDriver("test-device", Mock(), ui_adb_client=ui)
|
|
driver._device = device
|
|
controller = object.__new__(UnifiedMobileController)
|
|
controller._driver = driver
|
|
assert await controller.erase_text()
|
|
assert field["text"] == ""
|
|
ui.clear_text.assert_called_once_with()
|
|
device.shell.assert_not_called()
|
|
ui.set_clipboard.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("implementation", ["driver", "ime"])
|
|
@pytest.mark.parametrize("failure", [False, RuntimeError("write reply lost")])
|
|
async def test_failed_clear_never_types_or_retries(implementation, failure):
|
|
device, ui = Mock(), Mock()
|
|
if isinstance(failure, Exception):
|
|
ui.clear_text.side_effect = failure
|
|
else:
|
|
ui.clear_text.return_value = failure
|
|
if implementation == "driver":
|
|
driver = AndroidAdbDriver("test-device", Mock(), ui_adb_client=ui)
|
|
driver._device = device
|
|
result = await driver.input_text("replacement", clear_existing=True)
|
|
else:
|
|
result = await AndroidInputIME(device, ui).type_text("replacement", clear_existing=True)
|
|
assert result is False
|
|
ui.clear_text.assert_called_once_with()
|
|
device.shell.assert_not_called()
|
|
ui.set_clipboard.assert_not_called()
|
|
ui.send_text.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_clear_backend_is_not_reported_as_success():
|
|
driver = AndroidAdbDriver("test-device", Mock())
|
|
driver._device = Mock()
|
|
assert not await driver.input_text("replacement", clear_existing=True)
|
|
driver._device.shell.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("implementation", ["driver", "ime"])
|
|
async def test_empty_append_does_not_paste_or_send_keys(implementation):
|
|
device, ui = Mock(), Mock()
|
|
if implementation == "driver":
|
|
driver = AndroidAdbDriver("test-device", Mock(), ui_adb_client=ui)
|
|
driver._device = device
|
|
result = await driver.input_text("", clear_existing=False)
|
|
else:
|
|
result = await AndroidInputIME(device, ui).type_text("", clear_existing=False)
|
|
assert result is True
|
|
device.shell.assert_not_called()
|
|
ui.clear_text.assert_not_called()
|
|
ui.set_clipboard.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_partial_delete_stops_at_first_failed_key():
|
|
from unittest.mock import AsyncMock
|
|
|
|
controller = object.__new__(UnifiedMobileController)
|
|
controller._driver = Mock()
|
|
controller._driver.press_key = AsyncMock(return_value=False)
|
|
assert not await controller.erase_text(nb_chars=3)
|
|
controller._driver.press_key.assert_awaited_once_with("delete")
|