aboutsummaryrefslogtreecommitdiff
path: root/client/common_lib/ui_utils.py
blob: 5bc6b4faf0acfe95cd1a267c9955917ec2870326 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# Lint as: python2, python3
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import logging
import os
from six.moves import range
import time

from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import utils


class UIScreenshoter(object):
    """Simple class to take screenshots within the ui_utils framework."""

    _SCREENSHOT_DIR_PATH = '/var/log/ui_utils'
    _SCREENSHOT_BASENAME = 'ui-screenshot'

    def __init__(self):
        if not os.path.exists(self._SCREENSHOT_DIR_PATH):
            os.mkdir(self._SCREENSHOT_DIR_PATH, 0o755)
        self.screenshot_num = 0

    def take_ss(self):
        try:
            utils.run('screenshot "{}/{}_iter{}.png"'.format(
                      self._SCREENSHOT_DIR_PATH, self._SCREENSHOT_BASENAME,
                      self.screenshot_num))
            self.screenshot_num += 1
        except Exception as e:
            logging.warning('Unable to capture screenshot. %s', e)

class UI_Handler(object):

    REGEX_ALL = '/(.*?)/'

    PROMISE_TEMPLATE = \
        '''new Promise(function(resolve, reject) {
            chrome.automation.getDesktop(function(root) {
                    resolve(%s);
                })
            })'''

    _GET_ON_SCREEN_ITEMS = "findAll({attributes:{role: 'staticText'},state:{" \
                           "offscreen: false}}).map(node => node.name)"

    def __init__(self):
        self.screenshoter = UIScreenshoter()

    def start_ui_root(self, cr):
        """Start the UI root object for testing."""
        self.ext = cr.autotest_ext

    def is_obj_restricted(self, name, isRegex=False, role=None):
        """
        Return True if the object restriction is 'disabled'.
        This usually means the button is either greyed out, locked, etc.

        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: bool, if the item is a regex.
        @param role: Parameter to provide to the 'role' attribute.

        """
        FindParams = self._get_FindParams_str(name=name,
                                              role=role,
                                              isRegex=isRegex)
        try:
            restriction = self.ext.EvaluateJavaScript(
                self.PROMISE_TEMPLATE % ("%s.restriction" % FindParams),
                promise=True)
        except Exception:
            raise error.TestError(
                'Could not find object {}.'.format(name))
        if restriction == 'disabled':
            return True
        return False

    def item_present(self, name, isRegex=False, flip=False, role=None):
        """
        Determines if an object is present on the screen

        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: bool, if the 'name' is a regex.
        @param flip: Flips the return status.
        @param role: Parameter to provide to the 'role' attribute.

        @returns:
            True if object is present and flip is False.
            False if object is present and flip is True.
            False if object is not present and flip is False.
            True if object is not present and flip is True.

        """
        FindParams = self._get_FindParams_str(name=name,
                                              role=role,
                                              isRegex=isRegex)

        if flip is True:
            return not self._is_item_present(FindParams)
        return self._is_item_present(FindParams)

    def wait_for_ui_obj(self,
                        name,
                        isRegex=False,
                        remove=False,
                        role=None,
                        timeout=10):
        """
        Waits for the UI object specified.

        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: bool, if the 'name' is a regex.
        @param remove: bool, if you are waiting for the item to be removed.
        @param role: Parameter to provide to the 'role' attribute.
        @param timeout: int, time to wait for the item.

        @raises error.TestError if the element is not loaded (or removed).

        """
        try:
            utils.poll_for_condition(
                condition=lambda: self.item_present(name=name,
                                                    isRegex=isRegex,
                                                    flip=remove,
                                                    role=role),
                timeout=timeout,
                exception=error.TestError('{} did not load'
                                          .format(name)))
        except error.TestError:
            self.screenshoter.take_ss()
            logging.debug("CURRENT UI ITEMS VISIBLE {}".format(
                self.list_screen_items()))
            raise error.TestError('{} did not load'.format(name))

    def did_obj_not_load(self, name, isRegex=False, timeout=5):
        """
        Specifically used to wait and see if an item appears on the UI.

        NOTE: This is different from wait_for_ui_obj because that returns as
        soon as the object is either loaded or not loaded. This function will
        wait to ensure over the timeout period the object never loads.
        Additionally it will return as soon as it does load. Basically a fancy
        time.sleep()

        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: bool, if the item is a regex.
        @param timeout: Time in seconds to wait for the object to appear.

        @returns: True if object never loaded within the timeout period,
            else False.

        """
        t1 = time.time()
        while time.time() - t1 < timeout:
            if self.item_present(name=name, isRegex=isRegex):
                return False
            time.sleep(1)
        return True

    def doDefault_on_obj(self, name, isRegex=False, role=None):
        """Runs the .doDefault() js command on the element."""
        FindParams = self._get_FindParams_str(name=name,
                                              role=role,
                                              isRegex=isRegex)
        try:
            self.ext.EvaluateJavaScript(
                self.PROMISE_TEMPLATE % ("%s.doDefault()" % FindParams),
                promise=True)
        except:
            logging.info('Unable to .doDefault() on {}. All items: {}'
                         .format(FindParams, self.list_screen_items()))
            raise error.TestError("doDefault failed on {}".format(FindParams))

    def doCommand_on_obj(self, name, cmd, isRegex=False, role=None):
        """Run the specified command on the element."""
        FindParams = self._get_FindParams_str(name=name,
                                              role=role,
                                              isRegex=isRegex)
        return self.ext.EvaluateJavaScript(self.PROMISE_TEMPLATE % """
            %s.%s""" % (FindParams, cmd), promise=True)

    def list_screen_items(self,
                          role=None,
                          name=None,
                          isRegex=False,
                          attr='name'):

        """
        List all the items currently visable on the screen.

        If no paramters are given, it will return the name of each item,
        including items with empty names.

        @param role: The role of the items to use (ie button).
        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: bool, if the obj is a regex.
        @param attr: Str, the attribute you want returned in the list
            (eg 'name').

        """

        if isRegex:
            if name is None:
                raise error.TestError('If regex is True name must be given')
            name = self._format_obj(name, isRegex)
        elif name is not None:
            name = self._format_obj(name, isRegex)
        name = self.REGEX_ALL if name is None else name
        role = self.REGEX_ALL if role is None else self._format_obj(role,
                                                                    False)

        new_promise = self.PROMISE_TEMPLATE % """root.findAll({attributes:
            {name: %s, role: %s}}).map(node => node.%s)""" % (name, role, attr)
        return self.ext.EvaluateJavaScript(new_promise, promise=True)

    def get_name_role_list(self, name=None, role=None):
        """
        Return [{}, {}] containing the name/role of everything on screen.

        """
        name = self.REGEX_ALL if name is None else name
        role = self.REGEX_ALL if role is None else self._format_obj(role,
                                                                    False)

        new_promise = self.PROMISE_TEMPLATE % """root.findAll({attributes:
            {name: %s, role: %s}}).map(node =>
            {return {name: node.name, role: node.role} })""" % (name, role)

        return self.ext.EvaluateJavaScript(new_promise, promise=True)

    def _format_obj(self, name, isRegex):
        """
        Formats the object for use in the javascript name attribute.

        When searching for an element on the UI, a regex expression or string
        can be used. If the search is using a string, the obj will need to be
        wrapped in quotes. A Regex is not.

        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: if True, the object will be returned as is, if False
            the obj will be returned wrapped in quotes.

        @returns: The formatted string for regex/name.
        """
        if isRegex:
            return name
        else:
            return '"{}"'.format(name)

    def _get_FindParams_str(self, name, role, isRegex):
        """Returns the FindParms string, so that automation node functions
        can be run on it

        @param role: The role of the items to use (ie button).
        @param name: Parameter to provide to the 'name' attribute.
        @param isRegex: bool, if the obj is a regex.

        @returns: The ".find($FindParams)" string, which can be used to run
            automation node commands, such as .doDefault()

        """
        FINDPARAMS_BASE = """
        root.find({attributes:
                  {name: %s,
                   role: %s}}
                 )"""

        name = self._format_obj(name, isRegex)
        if role is None:
            role = self.REGEX_ALL
        else:
            role = self._format_obj(role, False)
        return (FINDPARAMS_BASE % (name, role))

    def _is_item_present(self, findParams):
        """Return False if tempVar is None, else True."""
        item_present = self.ext.EvaluateJavaScript(
            self.PROMISE_TEMPLATE % findParams,
            promise=True)
        if item_present is None:
            return False
        return True

    def click_and_wait_for_item_with_retries(self,
                                             item_to_click,
                                             item_to_wait_for,
                                             isRegex_click=False,
                                             isRegex_wait=False,
                                             click_role=None,
                                             wait_role=None):
        """
        Click on an item, and wait for a subsequent item to load. If the new
        item does not load, we attempt to click the button again.

        This being done to remove the corner case of button being visually
        loaded, but not fully ready to be clicked yet. In simple terms:
            Click button --> Check for next button to appear
            IF button does not appear, its likely the original button click did
            not work, thus reclick that button --> recheck for the next button.

            If button did appear, stop clicking.

        """
        self.doDefault_on_obj(item_to_click,
                              role=click_role,
                              isRegex=isRegex_click)
        for retry in range(3):
            try:
                self.wait_for_ui_obj(item_to_wait_for,
                                     role=wait_role,
                                     isRegex=isRegex_wait,
                                     timeout=6)
                break
            except error.TestError:
                self.doDefault_on_obj(item_to_click,
                                      role=click_role,
                                      isRegex=isRegex_click)
        else:
            raise error.TestError('Item {} did not load after 2 tries'.format(
                                  item_to_wait_for))