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 5"""Unit tests for the app_ui module.""" 6 7import unittest 8from xml.etree import ElementTree as element_tree 9 10from devil import devil_env 11from devil.android import app_ui 12from devil.android import device_errors 13from devil.utils import geometry 14 15with devil_env.SysPath(devil_env.PYMOCK_PATH): 16 import mock # pylint: disable=import-error 17 18 19MOCK_XML_LOADING = ''' 20<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> 21<hierarchy rotation="0"> 22 <node bounds="[0,50][1536,178]" content-desc="Loading" 23 resource-id="com.example.app:id/spinner"/> 24</hierarchy> 25'''.strip() 26 27 28MOCK_XML_LOADED = ''' 29<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> 30<hierarchy rotation="0"> 31 <node bounds="[0,50][1536,178]" content-desc="" 32 resource-id="com.example.app:id/toolbar"> 33 <node bounds="[0,58][112,170]" content-desc="Open navigation drawer"/> 34 <node bounds="[121,50][1536,178]" 35 resource-id="com.example.app:id/actionbar_custom_view"> 36 <node bounds="[121,50][1424,178]" 37 resource-id="com.example.app:id/actionbar_title" text="Primary"/> 38 <node bounds="[1424,50][1536,178]" content-desc="Search" 39 resource-id="com.example.app:id/actionbar_search_button"/> 40 </node> 41 </node> 42 <node bounds="[0,178][576,1952]" resource-id="com.example.app:id/drawer"> 43 <node bounds="[0,178][144,1952]" 44 resource-id="com.example.app:id/mini_drawer"> 45 <node bounds="[40,254][104,318]" resource-id="com.example.app:id/avatar"/> 46 <node bounds="[16,354][128,466]" content-desc="Primary" 47 resource-id="com.example.app:id/image_view"/> 48 <node bounds="[16,466][128,578]" content-desc="Social" 49 resource-id="com.example.app:id/image_view"/> 50 <node bounds="[16,578][128,690]" content-desc="Promotions" 51 resource-id="com.example.app:id/image_view"/> 52 </node> 53 </node> 54</hierarchy> 55'''.strip() 56 57 58class UiAppTest(unittest.TestCase): 59 60 def setUp(self): 61 self.device = mock.Mock() 62 self.device.pixel_density = 320 # Each dp pixel is 2 real pixels. 63 self.app = app_ui.AppUi(self.device, package='com.example.app') 64 self._setMockXmlScreenshots([MOCK_XML_LOADED]) 65 66 def _setMockXmlScreenshots(self, xml_docs): 67 """Mock self.app._GetRootUiNode to load nodes from some test xml_docs. 68 69 Each time the method is called it will return a UI node for each string 70 given in |xml_docs|, or rise a time out error when the list is exhausted. 71 """ 72 # pylint: disable=protected-access 73 def get_mock_root_ui_node(value): 74 if isinstance(value, Exception): 75 raise value 76 return app_ui._UiNode( 77 self.device, element_tree.fromstring(value), self.app.package) 78 79 xml_docs.append(device_errors.CommandTimeoutError('Timed out!')) 80 81 self.app._GetRootUiNode = mock.Mock( 82 side_effect=(get_mock_root_ui_node(doc) for doc in xml_docs)) 83 84 def assertNodeHasAttribs(self, node, attr): 85 # pylint: disable=protected-access 86 for key, value in attr.iteritems(): 87 self.assertEquals(node._GetAttribute(key), value) 88 89 def assertTappedOnceAt(self, x, y): 90 self.device.RunShellCommand.assert_called_once_with( 91 ['input', 'tap', str(x), str(y)], check_return=True) 92 93 def testFind_byText(self): 94 node = self.app.GetUiNode(text='Primary') 95 self.assertNodeHasAttribs(node, { 96 'text': 'Primary', 97 'content-desc': None, 98 'resource-id': 'com.example.app:id/actionbar_title', 99 }) 100 self.assertEquals(node.bounds, geometry.Rectangle([121, 50], [1424, 178])) 101 102 def testFind_byContentDesc(self): 103 node = self.app.GetUiNode(content_desc='Social') 104 self.assertNodeHasAttribs(node, { 105 'text': None, 106 'content-desc': 'Social', 107 'resource-id': 'com.example.app:id/image_view', 108 }) 109 self.assertEquals(node.bounds, geometry.Rectangle([16, 466], [128, 578])) 110 111 def testFind_byResourceId_autocompleted(self): 112 node = self.app.GetUiNode(resource_id='image_view') 113 self.assertNodeHasAttribs(node, { 114 'content-desc': 'Primary', 115 'resource-id': 'com.example.app:id/image_view', 116 }) 117 118 def testFind_byResourceId_absolute(self): 119 node = self.app.GetUiNode(resource_id='com.example.app:id/image_view') 120 self.assertNodeHasAttribs(node, { 121 'content-desc': 'Primary', 122 'resource-id': 'com.example.app:id/image_view', 123 }) 124 125 def testFind_byMultiple(self): 126 node = self.app.GetUiNode(resource_id='image_view', 127 content_desc='Promotions') 128 self.assertNodeHasAttribs(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( 146 node[0], {'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