diff options
Diffstat (limited to 'catapult/devil/devil/android/app_ui_test.py')
-rw-r--r-- | catapult/devil/devil/android/app_ui_test.py | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/catapult/devil/devil/android/app_ui_test.py b/catapult/devil/devil/android/app_ui_test.py new file mode 100644 index 00000000..34729851 --- /dev/null +++ b/catapult/devil/devil/android/app_ui_test.py @@ -0,0 +1,191 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Unit tests for the app_ui module.""" + +import unittest +from xml.etree import ElementTree as element_tree + +from devil import devil_env +from devil.android import app_ui +from devil.android import device_errors +from devil.utils import geometry + +with devil_env.SysPath(devil_env.PYMOCK_PATH): + import mock # pylint: disable=import-error + + +MOCK_XML_LOADING = ''' +<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> +<hierarchy rotation="0"> + <node bounds="[0,50][1536,178]" content-desc="Loading" + resource-id="com.example.app:id/spinner"/> +</hierarchy> +'''.strip() + + +MOCK_XML_LOADED = ''' +<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> +<hierarchy rotation="0"> + <node bounds="[0,50][1536,178]" content-desc="" + resource-id="com.example.app:id/toolbar"> + <node bounds="[0,58][112,170]" content-desc="Open navigation drawer"/> + <node bounds="[121,50][1536,178]" + resource-id="com.example.app:id/actionbar_custom_view"> + <node bounds="[121,50][1424,178]" + resource-id="com.example.app:id/actionbar_title" text="Primary"/> + <node bounds="[1424,50][1536,178]" content-desc="Search" + resource-id="com.example.app:id/actionbar_search_button"/> + </node> + </node> + <node bounds="[0,178][576,1952]" resource-id="com.example.app:id/drawer"> + <node bounds="[0,178][144,1952]" + resource-id="com.example.app:id/mini_drawer"> + <node bounds="[40,254][104,318]" resource-id="com.example.app:id/avatar"/> + <node bounds="[16,354][128,466]" content-desc="Primary" + resource-id="com.example.app:id/image_view"/> + <node bounds="[16,466][128,578]" content-desc="Social" + resource-id="com.example.app:id/image_view"/> + <node bounds="[16,578][128,690]" content-desc="Promotions" + resource-id="com.example.app:id/image_view"/> + </node> + </node> +</hierarchy> +'''.strip() + + +class UiAppTest(unittest.TestCase): + + def setUp(self): + self.device = mock.Mock() + self.device.pixel_density = 320 # Each dp pixel is 2 real pixels. + self.app = app_ui.AppUi(self.device, package='com.example.app') + self._setMockXmlScreenshots([MOCK_XML_LOADED]) + + def _setMockXmlScreenshots(self, xml_docs): + """Mock self.app._GetRootUiNode to load nodes from some test xml_docs. + + Each time the method is called it will return a UI node for each string + given in |xml_docs|, or rise a time out error when the list is exhausted. + """ + # pylint: disable=protected-access + def get_mock_root_ui_node(value): + if isinstance(value, Exception): + raise value + return app_ui._UiNode( + self.device, element_tree.fromstring(value), self.app.package) + + xml_docs.append(device_errors.CommandTimeoutError('Timed out!')) + + self.app._GetRootUiNode = mock.Mock( + side_effect=(get_mock_root_ui_node(doc) for doc in xml_docs)) + + def assertNodeHasAttribs(self, node, attr): + # pylint: disable=protected-access + for key, value in attr.iteritems(): + self.assertEquals(node._GetAttribute(key), value) + + def assertTappedOnceAt(self, x, y): + self.device.RunShellCommand.assert_called_once_with( + ['input', 'tap', str(x), str(y)], check_return=True) + + def testFind_byText(self): + node = self.app.GetUiNode(text='Primary') + self.assertNodeHasAttribs(node, { + 'text': 'Primary', + 'content-desc': None, + 'resource-id': 'com.example.app:id/actionbar_title', + }) + self.assertEquals(node.bounds, geometry.Rectangle([121, 50], [1424, 178])) + + def testFind_byContentDesc(self): + node = self.app.GetUiNode(content_desc='Social') + self.assertNodeHasAttribs(node, { + 'text': None, + 'content-desc': 'Social', + 'resource-id': 'com.example.app:id/image_view', + }) + self.assertEquals(node.bounds, geometry.Rectangle([16, 466], [128, 578])) + + def testFind_byResourceId_autocompleted(self): + node = self.app.GetUiNode(resource_id='image_view') + self.assertNodeHasAttribs(node, { + 'content-desc': 'Primary', + 'resource-id': 'com.example.app:id/image_view', + }) + + def testFind_byResourceId_absolute(self): + node = self.app.GetUiNode(resource_id='com.example.app:id/image_view') + self.assertNodeHasAttribs(node, { + 'content-desc': 'Primary', + 'resource-id': 'com.example.app:id/image_view', + }) + + def testFind_byMultiple(self): + node = self.app.GetUiNode(resource_id='image_view', + content_desc='Promotions') + self.assertNodeHasAttribs(node, { + 'content-desc': 'Promotions', + 'resource-id': 'com.example.app:id/image_view', + }) + self.assertEquals(node.bounds, geometry.Rectangle([16, 578], [128, 690])) + + def testFind_notFound(self): + node = self.app.GetUiNode(resource_id='does_not_exist') + self.assertIsNone(node) + + def testFind_noArgsGiven(self): + # Same exception given by Python for a function call with not enough args. + with self.assertRaises(TypeError): + self.app.GetUiNode() + + def testGetChildren(self): + node = self.app.GetUiNode(resource_id='mini_drawer') + self.assertNodeHasAttribs( + node[0], {'resource-id': 'com.example.app:id/avatar'}) + self.assertNodeHasAttribs(node[1], {'content-desc': 'Primary'}) + self.assertNodeHasAttribs(node[2], {'content-desc': 'Social'}) + self.assertNodeHasAttribs(node[3], {'content-desc': 'Promotions'}) + with self.assertRaises(IndexError): + # pylint: disable=pointless-statement + node[4] + + def testTap_center(self): + node = self.app.GetUiNode(content_desc='Open navigation drawer') + node.Tap() + self.assertTappedOnceAt(56, 114) + + def testTap_topleft(self): + node = self.app.GetUiNode(content_desc='Open navigation drawer') + node.Tap(geometry.Point(0, 0)) + self.assertTappedOnceAt(0, 58) + + def testTap_withOffset(self): + node = self.app.GetUiNode(content_desc='Open navigation drawer') + node.Tap(geometry.Point(10, 20)) + self.assertTappedOnceAt(10, 78) + + def testTap_withOffsetInDp(self): + node = self.app.GetUiNode(content_desc='Open navigation drawer') + node.Tap(geometry.Point(10, 20), dp_units=True) + self.assertTappedOnceAt(20, 98) + + def testTap_dpUnitsIgnored(self): + node = self.app.GetUiNode(content_desc='Open navigation drawer') + node.Tap(dp_units=True) + self.assertTappedOnceAt(56, 114) # Still taps at center. + + @mock.patch('time.sleep', mock.Mock()) + def testWaitForUiNode_found(self): + self._setMockXmlScreenshots( + [MOCK_XML_LOADING, MOCK_XML_LOADING, MOCK_XML_LOADED]) + node = self.app.WaitForUiNode(resource_id='actionbar_title') + self.assertNodeHasAttribs(node, {'text': 'Primary'}) + + @mock.patch('time.sleep', mock.Mock()) + def testWaitForUiNode_notFound(self): + self._setMockXmlScreenshots( + [MOCK_XML_LOADING, MOCK_XML_LOADING, MOCK_XML_LOADING]) + with self.assertRaises(device_errors.CommandTimeoutError): + self.app.WaitForUiNode(resource_id='actionbar_title') |