xref: /aosp_15_r20/external/autotest/client/common_lib/ui_utils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Lifrom __future__ import absolute_import
3*9c5db199SXin Lifrom __future__ import division
4*9c5db199SXin Lifrom __future__ import print_function
5*9c5db199SXin Li
6*9c5db199SXin Liimport logging
7*9c5db199SXin Liimport os
8*9c5db199SXin Lifrom six.moves import range
9*9c5db199SXin Liimport time
10*9c5db199SXin Li
11*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
12*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils
13*9c5db199SXin Li
14*9c5db199SXin Li
15*9c5db199SXin Liclass UIScreenshoter(object):
16*9c5db199SXin Li    """Simple class to take screenshots within the ui_utils framework."""
17*9c5db199SXin Li
18*9c5db199SXin Li    _SCREENSHOT_DIR_PATH = '/var/log/ui_utils'
19*9c5db199SXin Li    _SCREENSHOT_BASENAME = 'ui-screenshot'
20*9c5db199SXin Li
21*9c5db199SXin Li    def __init__(self):
22*9c5db199SXin Li        if not os.path.exists(self._SCREENSHOT_DIR_PATH):
23*9c5db199SXin Li            os.mkdir(self._SCREENSHOT_DIR_PATH, 0o755)
24*9c5db199SXin Li        self.screenshot_num = 0
25*9c5db199SXin Li
26*9c5db199SXin Li    def take_ss(self):
27*9c5db199SXin Li        try:
28*9c5db199SXin Li            utils.run('screenshot "{}/{}_iter{}.png"'.format(
29*9c5db199SXin Li                      self._SCREENSHOT_DIR_PATH, self._SCREENSHOT_BASENAME,
30*9c5db199SXin Li                      self.screenshot_num))
31*9c5db199SXin Li            self.screenshot_num += 1
32*9c5db199SXin Li        except Exception as e:
33*9c5db199SXin Li            logging.warning('Unable to capture screenshot. %s', e)
34*9c5db199SXin Li
35*9c5db199SXin Liclass UI_Handler(object):
36*9c5db199SXin Li
37*9c5db199SXin Li    REGEX_ALL = '/(.*?)/'
38*9c5db199SXin Li
39*9c5db199SXin Li    PROMISE_TEMPLATE = \
40*9c5db199SXin Li        '''new Promise(function(resolve, reject) {
41*9c5db199SXin Li            chrome.automation.getDesktop(function(root) {
42*9c5db199SXin Li                    resolve(%s);
43*9c5db199SXin Li                })
44*9c5db199SXin Li            })'''
45*9c5db199SXin Li
46*9c5db199SXin Li    _GET_ON_SCREEN_ITEMS = "findAll({attributes:{role: 'staticText'},state:{" \
47*9c5db199SXin Li                           "offscreen: false}}).map(node => node.name)"
48*9c5db199SXin Li
49*9c5db199SXin Li    def __init__(self):
50*9c5db199SXin Li        self.screenshoter = UIScreenshoter()
51*9c5db199SXin Li
52*9c5db199SXin Li    def start_ui_root(self, cr):
53*9c5db199SXin Li        """Start the UI root object for testing."""
54*9c5db199SXin Li        self.ext = cr.autotest_ext
55*9c5db199SXin Li
56*9c5db199SXin Li    def is_obj_restricted(self, name, isRegex=False, role=None):
57*9c5db199SXin Li        """
58*9c5db199SXin Li        Return True if the object restriction is 'disabled'.
59*9c5db199SXin Li        This usually means the button is either greyed out, locked, etc.
60*9c5db199SXin Li
61*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
62*9c5db199SXin Li        @param isRegex: bool, if the item is a regex.
63*9c5db199SXin Li        @param role: Parameter to provide to the 'role' attribute.
64*9c5db199SXin Li
65*9c5db199SXin Li        """
66*9c5db199SXin Li        FindParams = self._get_FindParams_str(name=name,
67*9c5db199SXin Li                                              role=role,
68*9c5db199SXin Li                                              isRegex=isRegex)
69*9c5db199SXin Li        try:
70*9c5db199SXin Li            restriction = self.ext.EvaluateJavaScript(
71*9c5db199SXin Li                self.PROMISE_TEMPLATE % ("%s.restriction" % FindParams),
72*9c5db199SXin Li                promise=True)
73*9c5db199SXin Li        except Exception:
74*9c5db199SXin Li            raise error.TestError(
75*9c5db199SXin Li                'Could not find object {}.'.format(name))
76*9c5db199SXin Li        if restriction == 'disabled':
77*9c5db199SXin Li            return True
78*9c5db199SXin Li        return False
79*9c5db199SXin Li
80*9c5db199SXin Li    def item_present(self, name, isRegex=False, flip=False, role=None):
81*9c5db199SXin Li        """
82*9c5db199SXin Li        Determines if an object is present on the screen
83*9c5db199SXin Li
84*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
85*9c5db199SXin Li        @param isRegex: bool, if the 'name' is a regex.
86*9c5db199SXin Li        @param flip: Flips the return status.
87*9c5db199SXin Li        @param role: Parameter to provide to the 'role' attribute.
88*9c5db199SXin Li
89*9c5db199SXin Li        @returns:
90*9c5db199SXin Li            True if object is present and flip is False.
91*9c5db199SXin Li            False if object is present and flip is True.
92*9c5db199SXin Li            False if object is not present and flip is False.
93*9c5db199SXin Li            True if object is not present and flip is True.
94*9c5db199SXin Li
95*9c5db199SXin Li        """
96*9c5db199SXin Li        FindParams = self._get_FindParams_str(name=name,
97*9c5db199SXin Li                                              role=role,
98*9c5db199SXin Li                                              isRegex=isRegex)
99*9c5db199SXin Li
100*9c5db199SXin Li        if flip is True:
101*9c5db199SXin Li            return not self._is_item_present(FindParams)
102*9c5db199SXin Li        return self._is_item_present(FindParams)
103*9c5db199SXin Li
104*9c5db199SXin Li    def wait_for_ui_obj(self,
105*9c5db199SXin Li                        name,
106*9c5db199SXin Li                        isRegex=False,
107*9c5db199SXin Li                        remove=False,
108*9c5db199SXin Li                        role=None,
109*9c5db199SXin Li                        timeout=10):
110*9c5db199SXin Li        """
111*9c5db199SXin Li        Waits for the UI object specified.
112*9c5db199SXin Li
113*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
114*9c5db199SXin Li        @param isRegex: bool, if the 'name' is a regex.
115*9c5db199SXin Li        @param remove: bool, if you are waiting for the item to be removed.
116*9c5db199SXin Li        @param role: Parameter to provide to the 'role' attribute.
117*9c5db199SXin Li        @param timeout: int, time to wait for the item.
118*9c5db199SXin Li
119*9c5db199SXin Li        @raises error.TestError if the element is not loaded (or removed).
120*9c5db199SXin Li
121*9c5db199SXin Li        """
122*9c5db199SXin Li        try:
123*9c5db199SXin Li            utils.poll_for_condition(
124*9c5db199SXin Li                condition=lambda: self.item_present(name=name,
125*9c5db199SXin Li                                                    isRegex=isRegex,
126*9c5db199SXin Li                                                    flip=remove,
127*9c5db199SXin Li                                                    role=role),
128*9c5db199SXin Li                timeout=timeout,
129*9c5db199SXin Li                exception=error.TestError('{} did not load'
130*9c5db199SXin Li                                          .format(name)))
131*9c5db199SXin Li        except error.TestError:
132*9c5db199SXin Li            self.screenshoter.take_ss()
133*9c5db199SXin Li            logging.debug("CURRENT UI ITEMS VISIBLE {}".format(
134*9c5db199SXin Li                self.list_screen_items()))
135*9c5db199SXin Li            raise error.TestError('{} did not load'.format(name))
136*9c5db199SXin Li
137*9c5db199SXin Li    def did_obj_not_load(self, name, isRegex=False, timeout=5):
138*9c5db199SXin Li        """
139*9c5db199SXin Li        Specifically used to wait and see if an item appears on the UI.
140*9c5db199SXin Li
141*9c5db199SXin Li        NOTE: This is different from wait_for_ui_obj because that returns as
142*9c5db199SXin Li        soon as the object is either loaded or not loaded. This function will
143*9c5db199SXin Li        wait to ensure over the timeout period the object never loads.
144*9c5db199SXin Li        Additionally it will return as soon as it does load. Basically a fancy
145*9c5db199SXin Li        time.sleep()
146*9c5db199SXin Li
147*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
148*9c5db199SXin Li        @param isRegex: bool, if the item is a regex.
149*9c5db199SXin Li        @param timeout: Time in seconds to wait for the object to appear.
150*9c5db199SXin Li
151*9c5db199SXin Li        @returns: True if object never loaded within the timeout period,
152*9c5db199SXin Li            else False.
153*9c5db199SXin Li
154*9c5db199SXin Li        """
155*9c5db199SXin Li        t1 = time.time()
156*9c5db199SXin Li        while time.time() - t1 < timeout:
157*9c5db199SXin Li            if self.item_present(name=name, isRegex=isRegex):
158*9c5db199SXin Li                return False
159*9c5db199SXin Li            time.sleep(1)
160*9c5db199SXin Li        return True
161*9c5db199SXin Li
162*9c5db199SXin Li    def doDefault_on_obj(self, name, isRegex=False, role=None):
163*9c5db199SXin Li        """Runs the .doDefault() js command on the element."""
164*9c5db199SXin Li        FindParams = self._get_FindParams_str(name=name,
165*9c5db199SXin Li                                              role=role,
166*9c5db199SXin Li                                              isRegex=isRegex)
167*9c5db199SXin Li        try:
168*9c5db199SXin Li            self.ext.EvaluateJavaScript(
169*9c5db199SXin Li                self.PROMISE_TEMPLATE % ("%s.doDefault()" % FindParams),
170*9c5db199SXin Li                promise=True)
171*9c5db199SXin Li        except:
172*9c5db199SXin Li            logging.info('Unable to .doDefault() on {}. All items: {}'
173*9c5db199SXin Li                         .format(FindParams, self.list_screen_items()))
174*9c5db199SXin Li            raise error.TestError("doDefault failed on {}".format(FindParams))
175*9c5db199SXin Li
176*9c5db199SXin Li    def doCommand_on_obj(self, name, cmd, isRegex=False, role=None):
177*9c5db199SXin Li        """Run the specified command on the element."""
178*9c5db199SXin Li        FindParams = self._get_FindParams_str(name=name,
179*9c5db199SXin Li                                              role=role,
180*9c5db199SXin Li                                              isRegex=isRegex)
181*9c5db199SXin Li        return self.ext.EvaluateJavaScript(self.PROMISE_TEMPLATE % """
182*9c5db199SXin Li            %s.%s""" % (FindParams, cmd), promise=True)
183*9c5db199SXin Li
184*9c5db199SXin Li    def list_screen_items(self,
185*9c5db199SXin Li                          role=None,
186*9c5db199SXin Li                          name=None,
187*9c5db199SXin Li                          isRegex=False,
188*9c5db199SXin Li                          attr='name'):
189*9c5db199SXin Li
190*9c5db199SXin Li        """
191*9c5db199SXin Li        List all the items currently visable on the screen.
192*9c5db199SXin Li
193*9c5db199SXin Li        If no paramters are given, it will return the name of each item,
194*9c5db199SXin Li        including items with empty names.
195*9c5db199SXin Li
196*9c5db199SXin Li        @param role: The role of the items to use (ie button).
197*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
198*9c5db199SXin Li        @param isRegex: bool, if the obj is a regex.
199*9c5db199SXin Li        @param attr: Str, the attribute you want returned in the list
200*9c5db199SXin Li            (eg 'name').
201*9c5db199SXin Li
202*9c5db199SXin Li        """
203*9c5db199SXin Li
204*9c5db199SXin Li        if isRegex:
205*9c5db199SXin Li            if name is None:
206*9c5db199SXin Li                raise error.TestError('If regex is True name must be given')
207*9c5db199SXin Li            name = self._format_obj(name, isRegex)
208*9c5db199SXin Li        elif name is not None:
209*9c5db199SXin Li            name = self._format_obj(name, isRegex)
210*9c5db199SXin Li        name = self.REGEX_ALL if name is None else name
211*9c5db199SXin Li        role = self.REGEX_ALL if role is None else self._format_obj(role,
212*9c5db199SXin Li                                                                    False)
213*9c5db199SXin Li
214*9c5db199SXin Li        new_promise = self.PROMISE_TEMPLATE % """root.findAll({attributes:
215*9c5db199SXin Li            {name: %s, role: %s}}).map(node => node.%s)""" % (name, role, attr)
216*9c5db199SXin Li        return self.ext.EvaluateJavaScript(new_promise, promise=True)
217*9c5db199SXin Li
218*9c5db199SXin Li    def get_name_role_list(self, name=None, role=None):
219*9c5db199SXin Li        """
220*9c5db199SXin Li        Return [{}, {}] containing the name/role of everything on screen.
221*9c5db199SXin Li
222*9c5db199SXin Li        """
223*9c5db199SXin Li        name = self.REGEX_ALL if name is None else name
224*9c5db199SXin Li        role = self.REGEX_ALL if role is None else self._format_obj(role,
225*9c5db199SXin Li                                                                    False)
226*9c5db199SXin Li
227*9c5db199SXin Li        new_promise = self.PROMISE_TEMPLATE % """root.findAll({attributes:
228*9c5db199SXin Li            {name: %s, role: %s}}).map(node =>
229*9c5db199SXin Li            {return {name: node.name, role: node.role} })""" % (name, role)
230*9c5db199SXin Li
231*9c5db199SXin Li        return self.ext.EvaluateJavaScript(new_promise, promise=True)
232*9c5db199SXin Li
233*9c5db199SXin Li    def _format_obj(self, name, isRegex):
234*9c5db199SXin Li        """
235*9c5db199SXin Li        Formats the object for use in the javascript name attribute.
236*9c5db199SXin Li
237*9c5db199SXin Li        When searching for an element on the UI, a regex expression or string
238*9c5db199SXin Li        can be used. If the search is using a string, the obj will need to be
239*9c5db199SXin Li        wrapped in quotes. A Regex is not.
240*9c5db199SXin Li
241*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
242*9c5db199SXin Li        @param isRegex: if True, the object will be returned as is, if False
243*9c5db199SXin Li            the obj will be returned wrapped in quotes.
244*9c5db199SXin Li
245*9c5db199SXin Li        @returns: The formatted string for regex/name.
246*9c5db199SXin Li        """
247*9c5db199SXin Li        if isRegex:
248*9c5db199SXin Li            return name
249*9c5db199SXin Li        else:
250*9c5db199SXin Li            return '"{}"'.format(name)
251*9c5db199SXin Li
252*9c5db199SXin Li    def _get_FindParams_str(self, name, role, isRegex):
253*9c5db199SXin Li        """Returns the FindParms string, so that automation node functions
254*9c5db199SXin Li        can be run on it
255*9c5db199SXin Li
256*9c5db199SXin Li        @param role: The role of the items to use (ie button).
257*9c5db199SXin Li        @param name: Parameter to provide to the 'name' attribute.
258*9c5db199SXin Li        @param isRegex: bool, if the obj is a regex.
259*9c5db199SXin Li
260*9c5db199SXin Li        @returns: The ".find($FindParams)" string, which can be used to run
261*9c5db199SXin Li            automation node commands, such as .doDefault()
262*9c5db199SXin Li
263*9c5db199SXin Li        """
264*9c5db199SXin Li        FINDPARAMS_BASE = """
265*9c5db199SXin Li        root.find({attributes:
266*9c5db199SXin Li                  {name: %s,
267*9c5db199SXin Li                   role: %s}}
268*9c5db199SXin Li                 )"""
269*9c5db199SXin Li
270*9c5db199SXin Li        name = self._format_obj(name, isRegex)
271*9c5db199SXin Li        if role is None:
272*9c5db199SXin Li            role = self.REGEX_ALL
273*9c5db199SXin Li        else:
274*9c5db199SXin Li            role = self._format_obj(role, False)
275*9c5db199SXin Li        return (FINDPARAMS_BASE % (name, role))
276*9c5db199SXin Li
277*9c5db199SXin Li    def _is_item_present(self, findParams):
278*9c5db199SXin Li        """Return False if tempVar is None, else True."""
279*9c5db199SXin Li        item_present = self.ext.EvaluateJavaScript(
280*9c5db199SXin Li            self.PROMISE_TEMPLATE % findParams,
281*9c5db199SXin Li            promise=True)
282*9c5db199SXin Li        if item_present is None:
283*9c5db199SXin Li            return False
284*9c5db199SXin Li        return True
285*9c5db199SXin Li
286*9c5db199SXin Li    def click_and_wait_for_item_with_retries(self,
287*9c5db199SXin Li                                             item_to_click,
288*9c5db199SXin Li                                             item_to_wait_for,
289*9c5db199SXin Li                                             isRegex_click=False,
290*9c5db199SXin Li                                             isRegex_wait=False,
291*9c5db199SXin Li                                             click_role=None,
292*9c5db199SXin Li                                             wait_role=None):
293*9c5db199SXin Li        """
294*9c5db199SXin Li        Click on an item, and wait for a subsequent item to load. If the new
295*9c5db199SXin Li        item does not load, we attempt to click the button again.
296*9c5db199SXin Li
297*9c5db199SXin Li        This being done to remove the corner case of button being visually
298*9c5db199SXin Li        loaded, but not fully ready to be clicked yet. In simple terms:
299*9c5db199SXin Li            Click button --> Check for next button to appear
300*9c5db199SXin Li            IF button does not appear, its likely the original button click did
301*9c5db199SXin Li            not work, thus reclick that button --> recheck for the next button.
302*9c5db199SXin Li
303*9c5db199SXin Li            If button did appear, stop clicking.
304*9c5db199SXin Li
305*9c5db199SXin Li        """
306*9c5db199SXin Li        self.doDefault_on_obj(item_to_click,
307*9c5db199SXin Li                              role=click_role,
308*9c5db199SXin Li                              isRegex=isRegex_click)
309*9c5db199SXin Li        for retry in range(3):
310*9c5db199SXin Li            try:
311*9c5db199SXin Li                self.wait_for_ui_obj(item_to_wait_for,
312*9c5db199SXin Li                                     role=wait_role,
313*9c5db199SXin Li                                     isRegex=isRegex_wait,
314*9c5db199SXin Li                                     timeout=6)
315*9c5db199SXin Li                break
316*9c5db199SXin Li            except error.TestError:
317*9c5db199SXin Li                self.doDefault_on_obj(item_to_click,
318*9c5db199SXin Li                                      role=click_role,
319*9c5db199SXin Li                                      isRegex=isRegex_click)
320*9c5db199SXin Li        else:
321*9c5db199SXin Li            raise error.TestError('Item {} did not load after 2 tries'.format(
322*9c5db199SXin Li                                  item_to_wait_for))
323