1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Unit tests for the app_ui module.""" 5 6import unittest 7from xml.etree import ElementTree as element_tree 8 9from devil import devil_env 10from devil.android import app_ui 11from devil.android import device_errors 12from devil.utils import geometry 13 14with devil_env.SysPath(devil_env.PYMOCK_PATH): 15 import mock # pylint: disable=import-error 16 17MOCK_XML_LOADING = ''' 18<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> 19<hierarchy rotation="0"> 20 <node bounds="[0,50][1536,178]" content-desc="Loading" 21 resource-id="com.example.app:id/spinner"/> 22</hierarchy> 23'''.strip() 24 25MOCK_XML_LOADED = ''' 26<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> 27<hierarchy rotation="0"> 28 <node bounds="[0,50][1536,178]" content-desc="" 29 resource-id="com.example.app:id/toolbar"> 30 <node bounds="[0,58][112,170]" content-desc="Open navigation drawer"/> 31 <node bounds="[121,50][1536,178]" 32 resource-id="com.example.app:id/actionbar_custom_view"> 33 <node bounds="[121,50][1424,178]" 34 resource-id="com.example.app:id/actionbar_title" text="Primary"/> 35 <node bounds="[1424,50][1536,178]" content-desc="Search" 36 resource-id="com.example.app:id/actionbar_search_button"/> 37 </node> 38 </node> 39 <node bounds="[0,178][576,1952]" resource-id="com.example.app:id/drawer"> 40 <node bounds="[0,178][144,1952]" 41 resource-id="com.example.app:id/mini_drawer"> 42 <node bounds="[40,254][104,318]" resource-id="com.example.app:id/avatar"/> 43 <node bounds="[16,354][128,466]" content-desc="Primary" 44 resource-id="com.example.app:id/image_view"/> 45 <node bounds="[16,466][128,578]" content-desc="Social" 46 resource-id="com.example.app:id/image_view"/> 47 <node bounds="[16,578][128,690]" content-desc="Promotions" 48 resource-id="com.example.app:id/image_view"/> 49 </node> 50 </node> 51</hierarchy> 52'''.strip() 53 54 55class UiAppTest(unittest.TestCase): 56 def setUp(self): 57 self.device = mock.Mock() 58 self.device.pixel_density = 320 # Each dp pixel is 2 real pixels. 59 self.app = app_ui.AppUi(self.device, package='com.example.app') 60 self._setMockXmlScreenshots([MOCK_XML_LOADED]) 61 62 def _setMockXmlScreenshots(self, xml_docs): 63 """Mock self.app._GetRootUiNode to load nodes from some test xml_docs. 64 65 Each time the method is called it will return a UI node for each string 66 given in |xml_docs|, or rise a time out error when the list is exhausted. 67 """ 68 69 # pylint: disable=protected-access 70 def get_mock_root_ui_node(value): 71 if isinstance(value, Exception): 72 raise value 73 return app_ui._UiNode(self.device, element_tree.fromstring(value), 74 self.app.package) 75 76 xml_docs.append(device_errors.CommandTimeoutError('Timed out!')) 77 78 self.app._GetRootUiNode = mock.Mock( 79 side_effect=(get_mock_root_ui_node(doc) for doc in xml_docs)) 80 81 def assertNodeHasAttribs(self, node, attr): 82 # pylint: disable=protected-access 83 for key, value in attr.items(): 84 self.assertEquals(node._GetAttribute(key), value) 85 86 def assertTappedOnceAt(self, x, y): 87 self.device.RunShellCommand.assert_called_once_with( 88 ['input', 'tap', str(x), str(y)], check_return=True) 89 90 def testFind_byText(self): 91 node = self.app.GetUiNode(text='Primary') 92 self.assertNodeHasAttribs( 93 node, { 94 'text': 'Primary', 95 'content-desc': None, 96 'resource-id': 'com.example.app:id/actionbar_title', 97 }) 98 self.assertEquals(node.bounds, geometry.Rectangle([121, 50], [1424, 178])) 99 100 def testFind_byContentDesc(self): 101 node = self.app.GetUiNode(content_desc='Social') 102 self.assertNodeHasAttribs( 103 node, { 104 'text': None, 105 'content-desc': 'Social', 106 'resource-id': 'com.example.app:id/image_view', 107 }) 108 self.assertEquals(node.bounds, geometry.Rectangle([16, 466], [128, 578])) 109 110 def testFind_byResourceId_autocompleted(self): 111 node = self.app.GetUiNode(resource_id='image_view') 112 self.assertNodeHasAttribs(node, { 113 'content-desc': 'Primary', 114 'resource-id': 'com.example.app:id/image_view', 115 }) 116 117 def testFind_byResourceId_absolute(self): 118 node = self.app.GetUiNode(resource_id='com.example.app:id/image_view') 119 self.assertNodeHasAttribs(node, { 120 'content-desc': 'Primary', 121 'resource-id': 'com.example.app:id/image_view', 122 }) 123 124 def testFind_byMultiple(self): 125 node = self.app.GetUiNode( 126 resource_id='image_view', content_desc='Promotions') 127 self.assertNodeHasAttribs( 128 node, { 129 'content-desc': 'Promotions', 130 'resource-id': 'com.example.app:id/image_view', 131 }) 132 self.assertEquals(node.bounds, geometry.Rectangle([16, 578], [128, 690])) 133 134 def testFind_notFound(self): 135 node = self.app.GetUiNode(resource_id='does_not_exist') 136 self.assertIsNone(node) 137 138 def testFind_noArgsGiven(self): 139 # Same exception given by Python for a function call with not enough args. 140 with self.assertRaises(TypeError): 141 self.app.GetUiNode() 142 143 def testGetChildren(self): 144 node = self.app.GetUiNode(resource_id='mini_drawer') 145 self.assertNodeHasAttribs(node[0], 146 {'resource-id': 'com.example.app:id/avatar'}) 147 self.assertNodeHasAttribs(node[1], {'content-desc': 'Primary'}) 148 self.assertNodeHasAttribs(node[2], {'content-desc': 'Social'}) 149 self.assertNodeHasAttribs(node[3], {'content-desc': 'Promotions'}) 150 with self.assertRaises(IndexError): 151 # pylint: disable=pointless-statement 152 node[4] 153 154 def testTap_center(self): 155 node = self.app.GetUiNode(content_desc='Open navigation drawer') 156 node.Tap() 157 self.assertTappedOnceAt(56, 114) 158 159 def testTap_topleft(self): 160 node = self.app.GetUiNode(content_desc='Open navigation drawer') 161 node.Tap(geometry.Point(0, 0)) 162 self.assertTappedOnceAt(0, 58) 163 164 def testTap_withOffset(self): 165 node = self.app.GetUiNode(content_desc='Open navigation drawer') 166 node.Tap(geometry.Point(10, 20)) 167 self.assertTappedOnceAt(10, 78) 168 169 def testTap_withOffsetInDp(self): 170 node = self.app.GetUiNode(content_desc='Open navigation drawer') 171 node.Tap(geometry.Point(10, 20), dp_units=True) 172 self.assertTappedOnceAt(20, 98) 173 174 def testTap_dpUnitsIgnored(self): 175 node = self.app.GetUiNode(content_desc='Open navigation drawer') 176 node.Tap(dp_units=True) 177 self.assertTappedOnceAt(56, 114) # Still taps at center. 178 179 @mock.patch('time.sleep', mock.Mock()) 180 def testWaitForUiNode_found(self): 181 self._setMockXmlScreenshots( 182 [MOCK_XML_LOADING, MOCK_XML_LOADING, MOCK_XML_LOADED]) 183 node = self.app.WaitForUiNode(resource_id='actionbar_title') 184 self.assertNodeHasAttribs(node, {'text': 'Primary'}) 185 186 @mock.patch('time.sleep', mock.Mock()) 187 def testWaitForUiNode_notFound(self): 188 self._setMockXmlScreenshots( 189 [MOCK_XML_LOADING, MOCK_XML_LOADING, MOCK_XML_LOADING]) 190 with self.assertRaises(device_errors.CommandTimeoutError): 191 self.app.WaitForUiNode(resource_id='actionbar_title') 192