xref: /aosp_15_r20/external/chromium-trace/catapult/devil/devil/android/app_ui_test.py (revision 1fa4b3da657c0e9ad43c0220bacf9731820715a5)
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