mcp-adb/tests/test_apps.py
Ryan Malloy fb297f7937 Add pytest suite (216 tests) and fix UI/notification parser bugs
Test infrastructure with conftest fixtures mocking run_shell_args/run_adb
for device-free testing across all 8 mixins.

Fixed: UI parser regex couldn't match hyphenated XML attributes
(content-desc, resource-id). Notification parser captured trailing
parenthesis in package names.
2026-02-11 03:38:37 -07:00

234 lines
8.6 KiB
Python

"""Tests for apps mixin (launch, close, current, install, intents)."""
import pytest
from tests.conftest import fail, ok
class TestAppLaunch:
async def test_launch(self, server):
server.run_shell_args.return_value = ok()
result = await server.app_launch("com.android.chrome")
assert result["success"] is True
assert result["package"] == "com.android.chrome"
args = server.run_shell_args.call_args[0][0]
assert "monkey" in args
assert "com.android.chrome" in args
async def test_failure(self, server):
server.run_shell_args.return_value = fail("not found")
result = await server.app_launch("com.missing.app")
assert result["success"] is False
class TestAppOpenUrl:
async def test_open(self, server):
server.run_shell_args.return_value = ok()
result = await server.app_open_url("https://example.com")
assert result["success"] is True
assert result["url"] == "https://example.com"
args = server.run_shell_args.call_args[0][0]
assert "am" in args
assert "android.intent.action.VIEW" in args
class TestAppClose:
async def test_close(self, server):
server.run_shell_args.return_value = ok()
result = await server.app_close("com.example.app")
assert result["success"] is True
assert result["package"] == "com.example.app"
args = server.run_shell_args.call_args[0][0]
assert "am" in args
assert "force-stop" in args
class TestAppCurrent:
async def test_parse_focused(self, server):
focused = (
" mCurrentFocus=Window{abc com.android.chrome"
"/org.chromium.chrome.browser.ChromeTabbedActivity}"
)
server.run_shell_args.return_value = ok(stdout=focused)
result = await server.app_current()
assert result["success"] is True
assert result["package"] == "com.android.chrome"
async def test_focused_app_format(self, server):
server.run_shell_args.return_value = ok(
stdout=" mFocusedApp=ActivityRecord{abc com.example/.MainActivity t123}"
)
result = await server.app_current()
assert result["success"] is True
assert result["package"] == "com.example"
async def test_no_focus(self, server):
server.run_shell_args.return_value = ok(stdout="no focus info")
result = await server.app_current()
assert result["success"] is True
assert result["package"] is None
class TestAppListPackages:
@pytest.mark.usefixtures("_dev_mode")
async def test_list(self, server):
server.run_shell_args.return_value = ok(
stdout="package:com.android.chrome\npackage:com.example.app\n"
)
result = await server.app_list_packages()
assert result["success"] is True
assert result["count"] == 2
assert "com.android.chrome" in result["packages"]
@pytest.mark.usefixtures("_dev_mode")
async def test_filter(self, server):
server.run_shell_args.return_value = ok(
stdout="package:com.android.chrome\npackage:com.example.app\n"
)
result = await server.app_list_packages(filter_text="chrome")
assert result["count"] == 1
assert "com.android.chrome" in result["packages"]
@pytest.mark.usefixtures("_dev_mode")
async def test_third_party(self, server):
server.run_shell_args.return_value = ok(stdout="package:com.user.app\n")
await server.app_list_packages(third_party_only=True)
args = server.run_shell_args.call_args[0][0]
assert "-3" in args
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.app_list_packages()
assert result["success"] is False
class TestAppInstall:
@pytest.mark.usefixtures("_dev_mode")
async def test_install(self, server):
server.run_adb.return_value = ok(stdout="Success")
result = await server.app_install("/tmp/app.apk")
assert result["success"] is True
args = server.run_adb.call_args[0][0]
assert "install" in args
assert "-r" in args
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.app_install("/tmp/app.apk")
assert result["success"] is False
class TestAppUninstall:
@pytest.mark.usefixtures("_dev_mode")
async def test_uninstall(self, server, ctx):
ctx.set_elicit("accept", "Yes, uninstall")
server.run_adb.return_value = ok()
result = await server.app_uninstall(ctx, "com.example.app")
assert result["success"] is True
assert result["package"] == "com.example.app"
@pytest.mark.usefixtures("_dev_mode")
async def test_keep_data(self, server, ctx):
ctx.set_elicit("accept", "Yes, uninstall")
server.run_adb.return_value = ok()
result = await server.app_uninstall(ctx, "com.example.app", keep_data=True)
assert result["kept_data"] is True
args = server.run_adb.call_args[0][0]
assert "-k" in args
@pytest.mark.usefixtures("_dev_mode")
async def test_cancelled(self, server, ctx):
ctx.set_elicit("accept", "Cancel")
result = await server.app_uninstall(ctx, "com.example.app")
assert result["success"] is False
assert result.get("cancelled") is True
class TestAppClearData:
@pytest.mark.usefixtures("_dev_mode")
async def test_clear(self, server, ctx):
ctx.set_elicit("accept", "Yes, clear all data")
server.run_shell_args.return_value = ok()
result = await server.app_clear_data(ctx, "com.example.app")
assert result["success"] is True
@pytest.mark.usefixtures("_dev_mode")
async def test_cancelled(self, server, ctx):
ctx.set_elicit("accept", "Cancel")
result = await server.app_clear_data(ctx, "com.example.app")
assert result.get("cancelled") is True
class TestActivityStart:
@pytest.mark.usefixtures("_dev_mode")
async def test_basic(self, server):
server.run_shell_args.return_value = ok()
result = await server.activity_start("com.example/.MainActivity")
assert result["success"] is True
assert result["component"] == "com.example/.MainActivity"
args = server.run_shell_args.call_args[0][0]
assert "am" in args
assert "start" in args
assert "-n" in args
@pytest.mark.usefixtures("_dev_mode")
async def test_with_action_and_data(self, server):
server.run_shell_args.return_value = ok()
await server.activity_start(
"com.example/.DeepLink",
action="android.intent.action.VIEW",
data_uri="myapp://product/123",
)
args = server.run_shell_args.call_args[0][0]
assert "-a" in args
assert "-d" in args
@pytest.mark.usefixtures("_dev_mode")
async def test_with_extras(self, server):
server.run_shell_args.return_value = ok()
await server.activity_start(
"com.example/.Act",
extras={"key": "value", "flag": "true", "count": "42"},
)
args = server.run_shell_args.call_args[0][0]
assert "--es" in args # string extra
assert "--ez" in args # boolean extra
assert "--ei" in args # integer extra
@pytest.mark.usefixtures("_dev_mode")
async def test_with_flags(self, server):
server.run_shell_args.return_value = ok()
await server.activity_start(
"com.example/.Act",
flags=["FLAG_ACTIVITY_NEW_TASK"],
)
args = server.run_shell_args.call_args[0][0]
assert "-f" in args
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.activity_start("com.example/.Act")
assert result["success"] is False
class TestBroadcastSend:
@pytest.mark.usefixtures("_dev_mode")
async def test_basic(self, server):
server.run_shell_args.return_value = ok()
result = await server.broadcast_send("com.example.ACTION")
assert result["success"] is True
assert result["broadcast_action"] == "com.example.ACTION"
@pytest.mark.usefixtures("_dev_mode")
async def test_with_package(self, server):
server.run_shell_args.return_value = ok()
await server.broadcast_send("ACTION", package="com.target")
args = server.run_shell_args.call_args[0][0]
assert "-p" in args
assert "com.target" in args
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.broadcast_send("ACTION")
assert result["success"] is False