Files
Artemis-Eapil/tests/unit/clients/test_accessibility_client.py
T
somew1nd bac31b44c1 feat(accessibility): implement Artemis Accessibility Helper for robust UI hierarchy capture
- Introduce ArtemisAccessibilityHelper APK (API 24 to 37+) as a conflict-free, high-performance replacement for UiAutomator:
  * Eliminates native UiAutomator waitForIdle() timeouts, reducing DOM dump latency from ~3s to ~15ms and maintaining resilience against infinite animations.
  * Implements a multi-tier fallback hierarchy discovery strategy (multi-window layer enumeration -> active window fallback -> input/accessibility focus backtracking) with adaptive progressive backoff for seamless page transition capture.
  * Resolves node drop defect caused by 32-bit hash collisions on recurring virtual view IDs in long lists and WebViews by utilizing bounded recursion (MAX_DEPTH=75, MAX_NODES=8000).
  * Strict W3C XML 1.0 character sanitization filtering invalid ASCII control codes and escaping attribute whitespace (
, 
, 	) to preserve multiline text formatting.
  * Enriches semantic parsing with stateDescription (API 30+ Jetpack Compose), screenReaderFocusable, paneTitle, heading, error, and tooltip attributes.
  * Multi-window metadata propagation tagging each node with window-id, window-type, and window-layer.
  * Adds dual-path standalone build script (build_apk.sh) producing a clean 29KB APK without Gradle daemon overhead alongside standard Gradle Kotlin DSL.
- Integrate AccessibilityClient into Artemis Python runtime with automatic bundled APK detection, self-healing installation, and port forwarding.
- Add unit test suite covering AccessibilityClient endpoints and atomic snapshot parsing.
2026-09-10 14:10:23 -07:00

89 lines
3.6 KiB
Python

import unittest
from unittest.mock import MagicMock, patch
from artemis.clients.accessibility_client import AccessibilityClient, DEFAULT_PORT
class TestAccessibilityClient(unittest.TestCase):
def setUp(self):
self.client = AccessibilityClient(device_id="dummy_device", local_port=DEFAULT_PORT)
@patch("artemis.clients.accessibility_client.urllib.request.urlopen")
def test_ping_success(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b'{"success": true, "service": "ArtemisAccessibilityService"}'
mock_resp.__enter__.return_value = mock_resp
mock_urlopen.return_value = mock_resp
self.assertTrue(self.client.ping())
@patch("artemis.clients.accessibility_client.urllib.request.urlopen")
def test_get_hierarchy(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b'{"success": true, "elements": [{"text": "OK"}]}'
mock_resp.__enter__.return_value = mock_resp
mock_urlopen.return_value = mock_resp
data = self.client.get_hierarchy()
self.assertTrue(data["success"])
self.assertEqual(data["elements"][0]["text"], "OK")
@patch("artemis.clients.accessibility_client.urllib.request.urlopen")
def test_tap_action(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b'{"success": true}'
mock_resp.__enter__.return_value = mock_resp
mock_urlopen.return_value = mock_resp
@patch("artemis.clients.accessibility_client.urllib.request.urlopen")
def test_get_hierarchy_xml(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b"<hierarchy rotation=\"0\"><node text=\"Save\" /></hierarchy>"
mock_resp.__enter__.return_value = mock_resp
mock_urlopen.return_value = mock_resp
xml = self.client.get_hierarchy_xml()
self.assertIn("<hierarchy", xml)
self.assertIn("Save", xml)
@patch.object(AccessibilityClient, "get_atomic_snapshot")
def test_get_screen_data_atomic(self, mock_atomic):
mock_atomic.return_value = {
"success": True,
"has_screenshot": True,
"screenshot_base64": "dummy_b64",
"xml": "<hierarchy rotation=\"0\"><node text=\"OK\" /></hierarchy>",
"elements": [{"text": "OK"}],
"width": 1080,
"height": 2400,
}
screen_data = self.client.get_screen_data()
self.assertEqual(screen_data.width, 1080)
self.assertEqual(screen_data.height, 2400)
self.assertEqual(screen_data.base64, "dummy_b64")
self.assertIn("rotation=\"0\"", screen_data.hierarchy_xml)
self.assertEqual(len(screen_data.elements), 1)
@patch.object(AccessibilityClient, "get_atomic_snapshot", return_value=None)
@patch.object(AccessibilityClient, "get_screenshot")
@patch.object(AccessibilityClient, "get_hierarchy")
def test_get_screen_data_fallback(self, mock_get_hierarchy, mock_get_screenshot, _mock_atomic):
from PIL import Image
mock_get_screenshot.return_value = Image.new("RGB", (1080, 2400))
mock_get_hierarchy.return_value = {
"success": True,
"xml": "<hierarchy rotation=\"0\"><node text=\"Fallback\" /></hierarchy>",
"elements": [{"text": "Fallback"}],
}
screen_data = self.client.get_screen_data()
self.assertEqual(screen_data.width, 1080)
self.assertEqual(screen_data.height, 2400)
self.assertIn("Fallback", screen_data.hierarchy_xml)
self.assertEqual(len(screen_data.elements), 1)
if __name__ == "__main__":
unittest.main()