summaryrefslogtreecommitdiff
path: root/openstack_dashboard/test/integration_tests/regions/tables.py
blob: 9778950b39e7eaebf667e831988c2ce122aa0f78 (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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import functools

from django.utils import html
from selenium.common import exceptions
from selenium.webdriver.common import by

from openstack_dashboard.test.integration_tests.regions import baseregion

NORMAL_COLUMN_CLASS = 'normal_column'


class RowRegion(baseregion.BaseRegion):
    """Classic table row."""

    _cell_locator = (by.By.CSS_SELECTOR, 'td.%s' % NORMAL_COLUMN_CLASS)
    _row_checkbox_locator = (
        by.By.CSS_SELECTOR,
        'td .themable-checkbox [type="checkbox"] + label'
    )

    def __init__(self, driver, conf, src_elem, column_names):
        self.column_names = column_names
        super().__init__(driver, conf, src_elem)

    @property
    def cells(self):
        elements = self._get_elements(*self._cell_locator)
        return {column_name: elements[i]
                for i, column_name in enumerate(self.column_names)}

    def mark(self):
        chck_box = self._get_element(*self._row_checkbox_locator)
        chck_box.click()


class TableRegion(baseregion.BaseRegion):
    """Basic class representing table object."""

    _heading_locator = (by.By.CSS_SELECTOR, 'h3.table_title')
    _columns_names_locator = (by.By.CSS_SELECTOR, 'thead > tr > th')
    _footer_locator = (by.By.CSS_SELECTOR, 'tfoot > tr > td > span')
    _rows_locator = (by.By.CSS_SELECTOR, 'tbody > tr')
    _empty_table_locator = (by.By.CSS_SELECTOR, 'tbody > tr.empty')
    _search_field_locator = (by.By.CSS_SELECTOR,
                             'div.table_search input.form-control')
    _search_button_locator = (by.By.CSS_SELECTOR,
                              'div.table_search > button')
    _search_option_locator = (by.By.CSS_SELECTOR,
                              'div.table_search > .themable-select')
    _cell_progress_bar_locator = (by.By.CSS_SELECTOR, 'div.progress-bar')
    _warning_cell_locator = (by.By.CSS_SELECTOR, 'td.warning')
    _default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
    marker_name = 'marker'
    prev_marker_name = 'prev_marker'

    def _table_locator(self, table_name):
        return by.By.CSS_SELECTOR, 'table#%s' % table_name

    @property
    def _next_locator(self):
        return by.By.CSS_SELECTOR, 'a[href^="?%s"]' % self.marker_name

    @property
    def _prev_locator(self):
        return by.By.CSS_SELECTOR, 'a[href^="?%s"]' % self.prev_marker_name

    def _search_menu_value_locator(self, value):
        return (by.By.CSS_SELECTOR,
                'ul.dropdown-menu a[data-select-value="%s"]' % value)

    def _cell_progress_bar_getter(self):
        return self.driver.find_element(*self._cell_progress_bar_locator)

    def _warning_cell_getter(self):
        return self.driver.find_element(*self._warning_cell_locator)

    def _form_getter(self):
        return self.driver.find_element(*self._default_form_locator)

    def __init__(self, driver, conf):
        self._default_src_locator = self._table_locator(self.__class__.name)
        super().__init__(driver, conf)

    @property
    def heading(self):
        return self._get_element(*self._heading_locator)

    @property
    def rows(self):
        if self._is_element_present(*self._empty_table_locator):
            return []
        else:
            return self._get_rows()

    @property
    def column_names(self):
        names = []
        for element in self._get_elements(*self._columns_names_locator):
            classes = element.get_attribute('class').split()
            if NORMAL_COLUMN_CLASS in classes:
                names.append(element.text)
        return names

    @property
    def footer(self):
        return self._get_element(*self._footer_locator)

    def filter(self, value):
        self._set_search_field(value)
        self._click_search_btn()
        self.driver.implicitly_wait(5)

    def set_filter_value(self, value):
        self.wait_till_element_disappears(self._form_getter)
        js_cmd = ("$('ul.dropdown-menu').find(\"a[data-select-value='%s']\")."
                  "click();" % (html.escape(value)))
        self.driver.execute_script(js_cmd)

    def get_row(self, column_name, text, exact_match=True):
        """Get row that contains specified text in specified column.

        In case exact_match is set to True, text contained in row must equal
        searched text, otherwise occurrence of searched text in the column
        text will result in row match.
        """
        def get_text(element):
            text = element.get_attribute('data-selenium')
            return text or element.text

        # wait until cells actions are completed eg: downloading image,
        # uploading image, creating, deleting etc.
        self.wait_till_element_disappears(self._cell_progress_bar_getter)
        self.wait_till_element_disappears(self._warning_cell_getter)

        for row in self.rows:
            try:
                cell = row.cells[column_name]
                if exact_match and text == get_text(cell):
                    return row
                if not exact_match and text in get_text(cell):
                    return row
            # NOTE(tsufiev): if a row was deleted during iteration
            except exceptions.StaleElementReferenceException:
                pass
        return None

    def _set_search_field(self, value):
        srch_field = self._get_element(*self._search_field_locator)
        srch_field.clear()
        srch_field.send_keys(value)

    def _click_search_btn(self):
        btn = self._get_element(*self._search_button_locator)
        btn.click()

    def _get_rows(self, *args):
        return [RowRegion(self.driver, self.conf, elem, self.column_names)
                for elem in self._get_elements(*self._rows_locator)]

    def _is_row_deleted(self, evaluator):
        def predicate(driver):
            if self._is_element_present(*self._empty_table_locator):
                return True
            with self.waits_disabled():
                return evaluator()
        try:
            self._wait_until(predicate)
        except exceptions.TimeoutException:
            return False
        except IndexError:
            return True
        return True

    def is_row_deleted(self, row_getter):
        return self._is_row_deleted(
            lambda: not self._is_element_displayed(row_getter()))

    def are_rows_deleted(self, rows_getter):
        # wait until rows are deleted.
        self.wait_till_element_disappears(self._warning_cell_getter)

        return self._is_row_deleted(
            lambda: all([not self._is_element_displayed(row) for row
                         in rows_getter()]))

    def wait_cell_status(self, cell_getter, statuses):
        if not isinstance(statuses, (list, tuple)):
            statuses = (statuses,)
        try:
            return self._wait_till_text_present_in_element(cell_getter,
                                                           statuses)
        except exceptions.TimeoutException:
            return False

    def is_next_link_available(self):
        try:
            self._turn_off_implicit_wait()
            return self._is_element_visible(*self._next_locator)
        finally:
            self._turn_on_implicit_wait()

    def is_prev_link_available(self):
        try:
            self._turn_off_implicit_wait()
            return self._is_element_visible(*self._prev_locator)
        finally:
            self._turn_on_implicit_wait()

    def turn_next_page(self):
        if self.is_next_link_available():
            lnk = self._get_element(*self._next_locator)
            lnk.click()

    def turn_prev_page(self):
        if self.is_prev_link_available():
            lnk = self._get_element(*self._prev_locator)
            lnk.click()

    def get_column_data(self, name_column='Name'):
        return [row.cells[name_column].text for row in self.rows]

    def assert_definition(self,
                          expected_table_definition,
                          sorting=False,
                          name_column='Name'):
        """Checks that actual table is expected one.

        Items to compare: 'next' and 'prev' links, count of rows and names of
        elements in list
        :param expected_table_definition: expected values (dictionary)
        :param sorting: boolean arg specifying whether to sort actual names
        :return:
        """
        names = self.get_column_data(name_column)
        if sorting:
            names.sort()
        actual_table = {'Next': self.is_next_link_available(),
                        'Prev': self.is_prev_link_available(),
                        'Count': len(self.rows),
                        'Names': names}
        self.assertDictEqual(actual_table, expected_table_definition)


def bind_table_action(action_name):
    """Decorator to bind table region method to an actual table action button.

    Many table actions when started (by clicking a corresponding button
    in UI) lead to some form showing up. To further interact with this form,
    a Python/ Selenium wrapper needs to be created for it. It is very
    convenient to return this newly created wrapper in the same method that
    initiates clicking an actual table action button. Binding the method to a
    button is performed behind the scenes in this decorator.

    .. param:: action_name

        Part of the action button id which is specific to action itself. It
        is safe to use action `name` attribute from the dashboard tables.py
        code.
    """
    _actions_locator = (by.By.CSS_SELECTOR, 'div.table_actions > button, a')

    def decorator(method):
        @functools.wraps(method)
        def wrapper(table):
            actions = table._get_elements(*_actions_locator)
            action_element = None
            for action in actions:
                target_action_id = '%s__action_%s' % (table.name, action_name)
                if action.get_attribute('id') == target_action_id:
                    action_element = action
                    break
            if action_element is None:
                msg = "Could not bind method '%s' to action control '%s'" % (
                    method.__name__, action_name)
                raise ValueError(msg)
            return method(table, action_element)
        return wrapper
    return decorator


def bind_row_action(action_name):
    """A decorator to bind table region method to an actual row action button.

    Many table actions when started (by clicking a corresponding button
    in UI) lead to some form showing up. To further interact with this form,
    a Python/ Selenium wrapper needs to be created for it. It is very
    convenient to return this newly created wrapper in the same method that
    initiates clicking an actual action button. Row action could be
    either primary (if its name is written right away on row action
    button) or secondary (if its name is inside of a button drop-down). Binding
    the method to a button and toggling the button drop-down open (in case
    a row action is secondary) is performed behind the scenes in this
    decorator.

    .. param:: action_name

        Part of the action button id which is specific to action itself. It
        is safe to use action `name` attribute from the dashboard tables.py
        code.
    """
    # NOTE(tsufiev): button tag could be either <a> or <button> - target
    # both with *. Also primary action could be single as well, do not use
    # .btn-group because of that
    primary_action_locator = (
        by.By.CSS_SELECTOR, 'td.actions_column *.btn:nth-child(1)')
    secondary_actions_opener_locator = (
        by.By.CSS_SELECTOR,
        'td.actions_column > .btn-group > *.btn:nth-child(2)')
    secondary_actions_locator = (
        by.By.CSS_SELECTOR,
        'td.actions_column > .btn-group > ul.row_actions > li > a, button')

    def decorator(method):
        @functools.wraps(method)
        def wrapper(table, row):
            def find_action(element):
                pattern = "__action_%s" % action_name
                return element.get_attribute('id').endswith(pattern)

            action_element = row._get_element(*primary_action_locator)
            if not find_action(action_element):
                action_element = None
                row._get_element(*secondary_actions_opener_locator).click()
                for element in row._get_elements(*secondary_actions_locator):
                    if find_action(element):
                        action_element = element
                        break

            if action_element is None:
                msg = "Could not bind method '%s' to action control '%s'" % (
                    method.__name__, action_name)
                raise ValueError(msg)
            return method(table, action_element, row)
        return wrapper
    return decorator


def bind_row_anchor_column(column_name):
    """A decorator to bind table region method to a anchor in a column.

    Typical examples of such tables are Project -> Compute -> Images, Admin
    -> System -> Flavors, Project -> Compute -> Instancies.
    The method can be used to follow the link in the anchor by the click.
    """

    def decorator(method):
        @functools.wraps(method)
        def wrapper(table, row):
            cell = row.cells[column_name]
            action_element = cell.find_element(
                by.By.CSS_SELECTOR, 'td.%s > a' % NORMAL_COLUMN_CLASS)
            return method(table, action_element, row)

        return wrapper
    return decorator