"""Provides a number of objects to help with managing certain elements in the CFME UI.
Specifically there are two categories of objects, organizational and elemental.
* **Organizational**
* :py:class:`Region`
* :py:mod:`cfme.web_ui.menu`
* **Elemental**
* :py:class:`AngularCalendarInput`
* :py:class:`ButtonGroup`
* :py:class:`Calendar`
* :py:class:`ColorGroup`
* :py:class:`CheckboxTable`
* :py:class:`CheckboxSelect`
* :py:class:`DHTMLSelect`
* :py:class:`DriftGrid`
* :py:class:`DynamicTable`
* :py:class:`EmailSelectForm`
* :py:class:`Filter`
* :py:class:`Form`
* :py:class:`InfoBlock`
* :py:class:`Input`
* :py:class:`MultiFill`
* :py:class:`Quadicon`
* :py:class:`Radio`
* :py:class:`ScriptBox`
* :py:class:`Select`
* :py:class:`ShowingInputs`
* :py:class:`SplitCheckboxTable`
* :py:class:`SplitTable`
* :py:class:`Timelines`
* :py:class:`Table`
* :py:class:`Tree`
* :py:mod:`cfme.web_ui.accordion`
* :py:mod:`cfme.web_ui.cfme_exception`
* :py:mod:`cfme.web_ui.flash`
* :py:mod:`cfme.web_ui.form_buttons`
* :py:mod:`cfme.web_ui.listaccordion`
* :py:mod:`cfme.web_ui.menu`
* :py:mod:`cfme.web_ui.paginator`
* :py:mod:`cfme.web_ui.snmp_form`
* :py:mod:`cfme.web_ui.tabstrip`
* :py:mod:`cfme.web_ui.toolbar`
"""
import os
import re
import types
from datetime import date
from collections import Sequence, Mapping, Callable
from xml.sax.saxutils import quoteattr
from selenium.common import exceptions as sel_exceptions
from selenium.common.exceptions import NoSuchElementException, MoveTargetOutOfBoundsException
from multimethods import multimethod, multidispatch, Anything
import cfme.fixtures.pytest_selenium as sel
from cfme import exceptions
from cfme.fixtures.pytest_selenium import browser
from utils import classproperty, lazycache, version
# For backward compatibility with code that pulls in Select from web_ui instead of sel
from cfme.fixtures.pytest_selenium import Select
from utils.log import logger
from utils.pretty import Pretty
[docs]class Region(Pretty):
"""
Base class for all UI regions/pages
Args:
locators: A dict of locator objects for the given region
title: A string containing the title of the page,
or a versioned dict of page title strings
identifying_loc: Single locator key from locators used by :py:meth:`Region.is_displayed`
to check if the region is currently visible
Usage:
page = Region(locators={
'configuration_button': (By.CSS_SELECTOR, "div.dhx_toolbar_btn[title='Configuration']"),
'discover_button': (By.CSS_SELECTOR,
"tr[title='Discover Cloud Providers']>td.td_btn_txt>" "div.btn_sel_text")
},
title='Cloud Providers',
identifying_loc='discover_button'
)
The elements can then accessed like so::
page.configuration_button
Locator attributes will return the locator tuple for that particular element,
and can be passed on to other functions, such as :py:func:`element` and :py:func:`click`.
Note:
When specifying a region title, omit the "Cloudforms Management Engine: " or "ManageIQ: "
prefix. They're included on every page, and different for the two versions of the appliance,
and :py:meth:`is_displayed` strips them off before checking for equality.
"""
pretty_attrs = ['title']
def __getattr__(self, name):
if hasattr(self, 'locators') and name in self.locators:
locator = self.locators[name]
if isinstance(locator, dict):
return version.pick(locator)
else:
return locator
else:
raise AttributeError("Region has no attribute named " + name)
def __init__(self, locators=None, title=None, identifying_loc=None, **kwargs):
self.locators = locators
self.identifying_loc = identifying_loc
self._title = title
self.infoblock = InfoBlock # Legacy support
@property
[docs] def title(self):
# support title being a versioned dict
if isinstance(self._title, dict):
self._title = version.pick(self._title)
return self._title
[docs] def is_displayed(self):
"""
Checks to see if the region is currently displayed.
Returns: A boolean describing if the region is currently displayed
"""
if not self.identifying_loc and not self.title:
logger.warning("Region doesn't have an identifying locator or title, "
"can't determine if current page.")
return True
# All page titles have a prefix; strip it off
try:
browser_title = browser().title.split(': ', 1)[1]
except IndexError:
browser_title = None
if self.identifying_loc and sel.is_displayed(
self.locators[self.identifying_loc], _no_deeper=True):
ident_match = True
else:
if not self.title:
logger.info('Identifying locator for region not found')
else:
logger.info('Identifying locator for region {} not found'.format(self.title))
ident_match = False
if self.title is None:
# If we don't have a title we can't match it, and some Regions are multi-page
# so we can't have a title set.
title_match = True
elif self.title and browser_title == self.title:
title_match = True
else:
logger.info("Title '%s' doesn't match expected title '%s'" %
(browser_title, self.title))
title_match = False
return title_match and ident_match
[docs]def get_context_current_page():
"""
Returns the current page name
Returns: A string containing the current page name
"""
url = browser().current_url()
stripped = url.lstrip('https://')
return stripped[stripped.find('/'):stripped.rfind('?')]
[docs]class Table(Pretty):
"""
Helper class for Table/List objects
Turns CFME custom Table/Lists into iterable objects using a generator.
Args:
table_locator: locator pointing to a table element with child thead and tbody elements
representing that table's header and body row containers
header_offset: In the case of a padding table row above the header, the row offset
can be used to skip rows in ``<thead>`` to locate the correct header row. This offset
is 1-indexed, not 0-indexed, so an offset of 1 is the first child row element
body_offset: In the case of a padding table row above the body rows, the row offset
can be used to skip rows in ``<ttbody>`` to locate the correct header row. This offset
is 1-indexed, not 0-indexed, so an offset of 1 is the first child row element
Attributes:
header_indexes: A dict of header names related to their int index as a column.
Usage:
table = Table('//div[@id="prov_pxe_img_div"]//table')
The HTML code for the table looks something like this::
<div id="prov_pxe_img_div">
<table>
<thead>
<tr>
<th>Name</th>
<th>Animal</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>Monkey</td>
<td>Small</td>
</tr>
<tr>
<td>Mike</td>
<td>Tiger</td>
<td>Large</td>
</tr>
</tbody>
</table>
</div>
We can now click on an element in the list like so, by providing the column
name and the value that we are searching for::
table.click_cell('name', 'Mike')
We can also perform the same, by using the index of the column, like so::
table.click_cell(1, 'Tiger')
Additionally, the rows of a table can be iterated over, and that row's columns can be accessed
by name or index (left to right, 0-index)::
for row in table.rows()
# Get the first cell in the row
row[0]
# Get the row's contents for the column with header 'Row Name'
# All of these will work, though the first is preferred
row.row_name, row['row_name'], row['Row Name']
When doing bulk opererations, such as selecting rows in a table based on their content,
the ``*_by_cells`` methods are able to find matching row much more quickly than iterating,
as the work can be done with fewer selenium calls.
* :py:meth:`find_rows_by_cells`
* :py:meth:`find_row_by_cells`
* :py:meth:`click_rows_by_cells`
* :py:meth:`click_row_by_cells`
Note:
A table is defined by the containers of the header and data areas, and offsets to them.
This allows a table to include one or more padding rows above the header row. In
the example above, there is no padding row, as our offset values are set to 0.
"""
pretty_attrs = ['_loc']
def __init__(self, table_locator, header_offset=0, body_offset=0):
self._headers = None
self._header_indexes = None
self._loc = table_locator
self.header_offset = int(header_offset)
self.body_offset = int(body_offset)
@property
@property
[docs] def body(self):
"""Property representing the ``<tbody>`` element that contains body rows"""
# tbody containing body rows
return sel.element('tbody', root=sel.element(self))
@property
@property
[docs] def locate(self):
return sel.move_to_element(self._loc)
@staticmethod
def _convert_header(header):
"""Convers header cell text into something usable as an identifier.
Static method which replaces spaces in headers with underscores and strips out
all other characters to give an identifier.
Args:
header: A header name to be converted.
Returns: A string holding the converted header.
"""
return re.sub('[^0-9a-zA-Z_]+', '', header.replace(' ', '_')).lower()
@property
def _root_loc(self):
return self.locate()
def _update_cache(self):
"""Updates the internal cache of headers
This allows columns to be moved and the Table updated. The :py:attr:`headers` stores
the header cache element and the list of headers are stored in _headers. The
attribute header_indexes is then created, before finally creating the items
attribute.
"""
self._headers = sel.elements('td | th', root=self.header_row)
self._header_indexes = {
self._convert_header(cell.text): self.headers.index(cell) for cell in self.headers}
[docs] def rows(self):
"""A generator method holding the Row objects
This generator yields Row objects starting at the first data row.
Yields:
:py:class:`Table.Row` object corresponding to the next row in the table.
"""
index = self.body_offset
row_elements = sel.elements('tr', root=self.body)
for row_element in row_elements[index:]:
yield self.create_row_from_element(row_element)
[docs] def find_row(self, header, value):
"""
Finds a row in the Table by iterating through each visible item.
Args:
header: A string or int, describing which column to inspect.
value: The value to be compared when trying to identify the correct row
to return.
Returns:
:py:class:`Table.Row` containing the requested cell, else ``None``.
"""
return self.find_row_by_cells({header: value})
[docs] def find_cell(self, header, value):
"""
Finds an item in the Table by iterating through each visible item,
this work used to be done by the :py:meth::`click_cell` method but
has not been abstracted out to be called separately.
Args:
header: A string or int, describing which column to inspect.
value: The value to be compared when trying to identify the correct cell
to click.
Returns: WebElement of the element if item was found, else ``None``.
"""
matching_cell_rows = self.find_rows_by_cells({header: value})
try:
if isinstance(header, basestring):
return getattr(matching_cell_rows[0], header)
else:
return matching_cell_rows[0][header]
except IndexError:
return None
[docs] def find_rows_by_cells(self, cells, partial_check=False):
"""A fast row finder, based on cell content.
Args:
cells: A dict of ``header: value`` pairs or a sequence of
nested ``(header, value)`` pairs.
Returns: A list of containing :py:class:`Table.Row` objects whose contents
match all of the header: value pairs in ``cells``
"""
# accept dicts or supertuples
cells = dict(cells)
cell_text_loc = (
'.//td/descendant-or-self::*[contains(normalize-space(text()), "%s")]/ancestor::tr[1]')
matching_rows_list = list()
for value in cells.values():
# Get all td elements that contain the value text
matching_elements = sel.elements(cell_text_loc % value,
root=sel.move_to_element(self._root_loc))
if matching_elements:
matching_rows_list.append(set(matching_elements))
# Now, find the common row elements that matched all the input cells
# (though not yet matching values to headers)
if not matching_rows_list:
# If none matched, short out
return []
rows_elements = list(reduce(lambda set1, set2: set1 & set2, matching_rows_list))
# Convert them to rows
# This is slow, which is why we do it after reducing the row element pile,
# and not when building matching_rows_list, but it makes comparing header
# names and expected values easy
rows = [self.create_row_from_element(element) for element in rows_elements]
# Only include rows where the expected values are in the right columns
matching_rows = list()
if partial_check:
matching_row_filter = lambda heading, value: value in row[heading].text
else:
matching_row_filter = lambda heading, value: row[heading].text == value
for row in rows:
if all(matching_row_filter(*cell) for cell in cells.items()):
matching_rows.append(row)
return matching_rows
[docs] def find_row_by_cells(self, cells, partial_check=False):
"""Find the first row containing cells
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
Returns: The first matching row found, or None if no matching row was found
"""
try:
rows = self.find_rows_by_cells(cells, partial_check=partial_check)
return rows[0]
except IndexError:
return None
[docs] def click_rows_by_cells(self, cells, click_column=None, partial_check=False):
"""Click the cell at ``click_column`` in the rows matched by ``cells``
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
click_column: Which column in the row to click, defaults to None,
which will attempt to click the row element
Note:
The value of click_column can be a string or an int, and will be passed directly to
the item accessor (``__getitem__``) for :py:class:`Table.Row`
"""
rows = self.find_rows_by_cells(cells, partial_check=partial_check)
if click_column is None:
map(sel.click, rows)
else:
map(sel.click, [row[click_column] for row in rows])
[docs] def click_row_by_cells(self, cells, click_column=None, partial_check=False):
"""Click the cell at ``click_column`` in the first row matched by ``cells``
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
click_column: See :py:meth:`Table.click_rows_by_cells`
"""
row = self.find_row_by_cells(cells, partial_check=partial_check)
if click_column is None:
sel.click(row)
else:
sel.click(row[click_column])
[docs] def create_row_from_element(self, row_element):
"""Given a row element in this table, create a :py:class:`Table.Row`
Args:
row_element: A table row (``<tr>``) WebElement representing a row in this table.
Returns: A :py:class:`Table.Row` for ``row_element``
"""
return Table.Row(row_element, self)
[docs] def click_cells(self, cell_map):
"""Submits multiple cells to be clicked on
Args:
cell_map: A mapping of header names and values, representing cells to click.
As an example, ``{'name': ['wing', 'nut']}, {'age': ['12']}`` would click on
the cells which had ``wing`` and ``nut`` in the name column and ``12`` in
the age column. The yaml example for this would be as follows::
list_items:
name:
- wing
- nut
age:
- 12
Raises:
NotAllItemsClicked: If some cells were unable to be found.
"""
failed_clicks = []
for header, values in cell_map.items():
if isinstance(values, basestring):
values = [values]
for value in values:
res = self.click_cell(header, value)
if not res:
failed_clicks.append("%s:%s" % (header, value))
if failed_clicks:
raise exceptions.NotAllItemsClicked(failed_clicks)
[docs] def click_cell(self, header, value):
"""Clicks on a cell defined in the row.
Uses the header identifier and a value to determine which cell to click on.
Args:
header: A string or int, describing which column to inspect.
value: The value to be compared when trying to identify the correct cell
to click the cell in.
Returns: ``True`` if item was found and clicked, else ``False``.
"""
cell = self.find_cell(header, value)
if cell:
sel.click(cell)
return True
else:
return False
[docs] class Row(Pretty):
"""An object representing a row in a Table.
The Row object returns a dymanically addressable attribute space so that
the tables headers are automatically generated.
Args:
row_element: A table row ``WebElement``
parent_table: :py:class:`Table` containing ``row_element``
Notes:
Attributes are dynamically generated. The index/key accessor is more flexible
than the attr accessor, as it can operate on int indices and header names.
"""
pretty_attrs = ['row_element', 'table']
def __init__(self, row_element, parent_table):
self.table = parent_table
self.row_element = row_element
@property
[docs] def columns(self):
"""A list of WebElements corresponding to the ``<td>`` elements in this row"""
return sel.elements('td', root=self.row_element)
def __getattr__(self, name):
"""
Returns Row element by header name
"""
return self.columns[self.table.header_indexes[name.lower()]]
def __getitem__(self, index):
"""
Returns Row element by header index or name
"""
try:
return self.columns[index]
except TypeError:
# Index isn't an int, assume it's a string
return getattr(self, self.table._convert_header(index))
# Let IndexError raise
def __str__(self):
return ", ".join(["'%s'" % el.text for el in self.columns])
def __eq__(self, other):
if isinstance(other, type(self)):
# Selenium elements support equality checks, so we can, too.
return self.row_element == other.row_element
else:
return id(self) == id(other)
[docs] def locate(self):
# table.create_row_from_element(row_instance) might actually work...
return sel.move_to_element(self.row_element)
[docs]class SplitTable(Table):
""":py:class:`Table` that supports the header and body rows being in separate tables
Args:
header_data: A tuple, containing an element locator and an offset value.
These point to the container of the header row. The offset is used in case
there is a padding row above the header, or in the case that the header
and the body are contained inside the same table element.
body_data: A tuple, containing an element locator and an offset value.
These point to the container of the body rows. The offset is used in case
there is a padding row above the body rows, or in the case that the header
and the body are contained inside the same table element.
Usage:
table = SplitTable(header_data=('//div[@id="header_table"]//table/tbody', 0),
body_data=('//div[@id="body_table"]//table/tbody', 1))
The HTML code for a split table looks something like this::
<div id="prov_pxe_img_div">
<table id="header_table">
<tbody>
<tr>
<td>Name</td>
<td>Animal</td>
<td>Size</td>
</tr>
</tbody>
</table>
<table id="body_table">
<tbody>
<tr>
<td>Useless</td>
<td>Padding</td>
<td>Row</td>
</tr>
<tr>
<td>John</td>
<td>Monkey</td>
<td>Small</td>
</tr>
<tr>
<td>Mike</td>
<td>Tiger</td>
<td>Large</td>
</tr>
</tbody>
</table>
</div>
Note the use of the offset to skip the "Useless Padding Row" in ``body_data``. Most split
tables require an offset for both the heading and body rows.
"""
def __init__(self, header_data, body_data):
self._headers = None
self._header_indexes = None
self._header_loc, header_offset = header_data
self._body_loc, body_offset = body_data
self.header_offset = int(header_offset)
self.body_offset = int(body_offset)
@property
def _root_loc(self):
return self._body_loc
@property
@property
[docs] def body(self):
"""Property representing the element that contains body rows"""
# tbody containing body rows
return sel.element(self._body_loc)
[docs] def locate(self):
# Use the header locator as the overall table locator
return sel.move_to_element(self._header_loc)
[docs]class SortTable(Table):
"""This table is the same as :py:class:`Table`, but with added sorting functionality."""
@property
def _sort_by_cell(self):
try:
return sel.element(
version.pick({
"default": "./th/a/img[contains(@src, 'sort')]/..",
"5.3.0.0": "./th[contains(@class, 'sorting_')]"
}),
root=self.header_row
)
except NoSuchElementException:
return None
@property
[docs] def sorted_by(self):
"""Return column name what is used for sorting now.
"""
cell = self._sort_by_cell
if cell is None:
return None
return sel.text("./a", root=cell).encode("utf-8")
@property
[docs] def sort_order(self):
"""Return order.
Returns: 'ascending' or 'descending'
"""
cell = self._sort_by_cell
if cell is None:
return None
def _downstream():
src = sel.get_attribute(sel.element("./img", root=cell), "src")
if "sort_up" in src:
return "ascending"
elif "sort_down" in src:
return "descending"
else:
return None
def _upstream():
cls = sel.get_attribute(cell, "class")
if "sorting_asc" in cls:
return "ascending"
elif "sorting_desc" in cls:
return "descending"
else:
return None
return version.pick({
"default": _downstream,
"5.3.0.0": _upstream
})()
[docs] def sort_by(self, header, order):
"""Sorts the table by given conditions
Args:
header: Text of the header cell to use for sorting.
order: ascending or descending
"""
order = order.lower().strip()
if header != self.sorted_by:
# Change column to order by
self.click_header_cell(header)
assert self.sorted_by == header, "Detected malfunction in table ordering"
if order != self.sort_order:
# Change direction
self.click_header_cell(header)
assert self.sort_order == order, "Detected malfunction in table ordering"
[docs]class CheckboxTable(Table):
""":py:class:`Table` with support for checkboxes
Args:
table_locator: See :py:class:`cfme.web_ui.Table`
header_checkbox_locator: Locator of header checkbox (default `None`)
Specify in case the header checkbox is not part of the header row
body_checkbox_locator: Locator for checkboxes in body rows
header_offset: See :py:class:`cfme.web_ui.Table`
body_offset: See :py:class:`cfme.web_ui.Table`
"""
_checkbox_loc = ".//input[@type='checkbox']"
def __init__(self, table_locator, header_offset=0, body_offset=0,
header_checkbox_locator=None, body_checkbox_locator=None):
super(CheckboxTable, self).__init__(table_locator, header_offset, body_offset)
if body_checkbox_locator:
self._checkbox_loc = body_checkbox_locator
self._header_checkbox_loc = header_checkbox_locator
@property
[docs] def select_all(self):
"""Select all rows using the header checkbox or one by one if not present"""
if self._header_checkbox_loc is None:
for row in self.rows():
self._set_row_checkbox(row, True)
else:
sel.uncheck(self.header_checkbox)
sel.check(self.header_checkbox)
[docs] def deselect_all(self):
"""Deselect all rows using the header checkbox or one by one if not present"""
if self._header_checkbox_loc is None:
for row in self.rows():
self._set_row_checkbox(row, False)
else:
sel.check(self.header_checkbox)
sel.uncheck(self.header_checkbox)
def _set_row_checkbox(self, row, set_to=False):
row_checkbox = sel.element(self._checkbox_loc, root=row.locate())
sel.checkbox(row_checkbox, set_to)
def _set_row(self, header, value, set_to=False):
""" Internal method used to select/deselect a row by column header and cell value
Args:
header: See :py:meth:`Table.find_row`
value: See :py:meth:`Table.find_row`
set_to: Select if `True`, deselect if `False`
"""
row = self.find_row(header, value)
if row:
self._set_row_checkbox(row, set_to)
return True
else:
return False
[docs] def select_rows_by_indexes(self, *indexes):
"""Select rows specified by row indexes (starting with 0)
"""
for i, row in enumerate(self.rows()):
if i in indexes:
self._set_row_checkbox(row, True)
[docs] def deselect_rows_by_indexes(self, *indexes):
"""Deselect rows specified by row indexes (starting with 0)
"""
for i, row in enumerate(self.rows()):
if i in indexes:
self._set_row_checkbox(row, False)
[docs] def select_row(self, header, value):
"""Select a single row specified by column header and cell value
Args:
header: See :py:meth:`Table.find_row`
value: See :py:meth:`Table.find_row`
Returns: `True` if successful, `False` otherwise
"""
return self._set_row(header, value, True)
[docs] def deselect_row(self, header, value):
"""Deselect a single row specified by column header and cell value
Args:
header: See :py:meth:`Table.find_row`
value: See :py:meth:`Table.find_row`
Returns: `True` if successful, `False` otherwise
"""
return self._set_row(header, value, False)
def _set_rows(self, cell_map, set_to=False):
""" Internal method used to select/deselect multiple rows
Args:
cell_map: See :py:meth:`Table.click_cells`
set_to: Select if `True`, deselect if `False`
"""
failed_selects = []
for header, values in cell_map.items():
if isinstance(values, basestring):
values = [values]
for value in values:
res = self._set_row(header, value, set_to)
if not res:
failed_selects.append("%s:%s" % (header, value))
if failed_selects:
raise exceptions.NotAllCheckboxesFound(failed_selects)
[docs] def select_rows(self, cell_map):
"""Select multiple rows
Args:
cell_map: See :py:meth:`Table.click_cells`
Raises:
NotAllCheckboxesFound: If some cells were unable to be found
"""
self._set_rows(cell_map, True)
[docs] def deselect_rows(self, cell_map):
"""Deselect multiple rows
Args:
cell_map: See :py:meth:`Table.click_cells`
Raises:
NotAllCheckboxesFound: If some cells were unable to be found
"""
self._set_rows(cell_map, False)
def _set_row_by_cells(self, cells, set_to=False):
row = self.find_row_by_cells(cells)
self._set_row_checkbox(row, set_to)
[docs] def select_row_by_cells(self, cells):
"""Select the first row matched by ``cells``
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
"""
self._set_row_by_cells(cells, True)
[docs] def deselect_row_by_cells(self, cells):
"""Deselect the first row matched by ``cells``
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
"""
self._set_row_by_cells(cells, False)
def _set_rows_by_cells(self, cells, set_to=False):
rows = self.find_rows_by_cells(cells)
for row in rows:
self._set_row_checkbox(row, set_to)
[docs] def select_rows_by_cells(self, cells):
"""Select the rows matched by ``cells``
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
"""
self._set_rows_by_cells(cells, True)
[docs] def deselect_rows_by_cells(self, cells):
"""Deselect the rows matched by ``cells``
Args:
cells: See :py:meth:`Table.find_rows_by_cells`
"""
self._set_rows_by_cells(cells, False)
[docs]class SplitCheckboxTable(SplitTable, CheckboxTable):
""":py:class:`SplitTable` with support for checkboxes
Args:
header_data: See :py:class:`cfme.web_ui.SplitTable`
body_data: See :py:class:`cfme.web_ui.SplitTable`
header_checkbox_locator: See :py:class:`cfme.web_ui.CheckboxTable`
body_checkbox_locator: See :py:class:`cfme.web_ui.CheckboxTable`
header_offset: See :py:class:`cfme.web_ui.Table`
body_offset: See :py:class:`cfme.web_ui.Table`
"""
_checkbox_loc = './/img[contains(@src, "item_chk")]'
def __init__(self, header_data, body_data,
header_checkbox_locator=None, body_checkbox_locator=None):
# To limit multiple inheritance surprises, explicitly call out to SplitTable's __init__
SplitTable.__init__(self, header_data, body_data)
# ...then set up CheckboxTable's locators here
self._header_checkbox_loc = header_checkbox_locator
if body_checkbox_locator:
self._checkbox_loc = body_checkbox_locator
[docs]def table_in_object(table_title):
"""If you want to point to tables inside object view, this is what you want to use.
Works both on down- and upstream.
Args:
table_title: Text in `p` element preceeding the table
Returns: XPath locator for the desired table.
"""
return ("//table[(preceding-sibling::p[1] | preceding-sibling::h3[1])[normalize-space(.)={}]]"
.format(quoteattr(table_title)))
@multimethod(lambda loc, value: (sel.tag(loc), sel.get_attribute(loc, 'type')))
def fill_tag(loc, value):
""" Return a tuple of function to do the filling, and a value to log."""
raise NotImplementedError("Don't know how to fill {} into this type: {}".format(value, loc))
@fill_tag.method(("select", Anything))
[docs]def fill_select_tag(select, value):
return (sel.select, value)
@fill_tag.method((Anything, 'text'))
@fill_tag.method((Anything, 'textarea'))
[docs]def fill_text(textbox, val):
return (sel.set_text, val)
@fill_tag.method((Anything, 'password'))
[docs]def fill_password(pwbox, password):
return (sel.set_text, "********")
@fill_tag.method(('a', Anything))
@fill_tag.method(('img', Anything))
@fill_tag.method((Anything, 'image'))
@fill_tag.method((Anything, 'submit'))
[docs]def fill_click(el, val):
"""Click only when given a truthy value"""
def click_if(e, v):
if v:
sel.click(e)
return (click_if, val)
@fill_tag.method((Anything, 'file'))
[docs]def fill_file(fd, val):
return (sel.send_keys, val)
@fill_tag.method((Anything, 'checkbox'))
[docs]def fill_checkbox(cb, val):
return (sel.checkbox, bool(val))
@multidispatch
def fill(loc, content):
"""
Fills in a UI component with the given content.
Usage:
fill(textbox, "text to fill")
fill(myform, [ ... data to fill ...])
fill(radio, "choice to select")
Returns: True if any UI action was taken, False otherwise
"""
action, logval = fill_tag(loc, content)
if hasattr(loc, 'name'):
ident = loc.name
else:
ident = loc
logger.debug(' Filling in [%s], with value "%s"' % (ident, logval))
prev_state = action(loc, content)
sel.detect_observed_field(loc)
return prev_state
@fill.method((Mapping, Anything))
def _version_pick(m, a, **kwargs):
return fill(version.pick(m), a, **kwargs)
@fill.method((Table, Mapping))
def _sd_fill_table(table, cells):
""" How to fill a table with a value (by selecting the value as cells in the table)
See Table.click_cells
"""
table._update_cache()
logger.debug(' Clicking Table cell')
table.click_cells(cells)
return bool(cells)
@fill.method((CheckboxTable, object))
def _sd_fill_checkboxtable(table, cells):
""" How to fill a checkboxtable with a value (by selecting the right rows)
See CheckboxTable.select_by_cells
"""
table._update_cache()
logger.debug(' Selecting CheckboxTable row')
table.select_rows(cells)
return bool(cells)
@fill.method((Callable, object))
[docs]def fill_callable(f, val):
"""Fill in a Callable by just calling it with the value, allow for arbitrary actions"""
return f(val)
@fill.method((Select, types.NoneType))
@fill.method((Select, object))
[docs]def fill_select(slist, val):
logger.debug(' Filling in {} with value {}'.format(str(slist), val))
prev_sel = sel.select(slist, val)
slist.observer_wait()
return prev_sel
[docs]class Calendar(Pretty):
"""A CFME calendar form field
Calendar fields are readonly, and managed by the dxhtmlCalendar widget. A Calendar field
will accept any object that can be coerced into a string, but the value may not match the format
expected by dhtmlxCalendar or CFME. For best results, either a ``datetime.date`` or
``datetime.datetime`` object should be used to create a valid date field.
Args:
name: "name" property of the readonly calendar field.
Usage:
calendar = web_ui.Calendar("miq_date_1")
web_ui.fill(calendar, date(2000, 1, 1))
web_ui.fill(calendar, '1/1/2001')
"""
def __init__(self, name):
self.name = name
[docs] def locate(self):
return sel.move_to_element(Input(self.name))
@fill.method((Calendar, object))
def _sd_fill_date(calendar, value):
input = sel.element(calendar)
if isinstance(value, date):
date_str = '%s/%s/%s' % (value.month, value.day, value.year)
else:
date_str = str(value)
# need to write to a readonly field: resort to evil
if sel.get_attribute(input, 'ng-model') is not None:
sel.set_angularjs_value(input, date_str)
else:
sel.set_attribute(input, "value", date_str)
# Now when we set the value, we need to simulate a change event.
if sel.get_attribute(input, "data-date-autoclose"):
# New one
script = "$(\"#%s\").trigger('changeDate');"
else:
# Old one
script = (
"if(typeof $j == 'undefined') {var jq = $;} else {var jq = $j;} "
"jq(\"#%s\").change();")
try:
sel.execute_script(script % calendar.name)
except sel_exceptions.WebDriverException as e:
logger.warning(
"An exception was raised during handling of the Cal #{}'s change event:\n{}"
.format(calendar.name, str(e)))
sel.wait_for_ajax()
return True
@fill.method((object, types.NoneType))
@fill.method((types.NoneType, object))
def _sd_fill_none(*args, **kwargs):
""" Ignore a NoneType """
pass
@fill.method((Form, Sequence))
def _fill_form_list(form, values, action=None, action_always=False):
"""Fills in field elements on forms
Takes a set of values in dict or supertuple format and locates form elements,
in the correct order, and fills them in.
Note:
Currently supports, text, textarea, select, checkbox, radio, password, a
and Table objects/elements.
Args:
values: a dict or supertuple formatted set of data where
each key is the name of the form locator from the page model. Some
objects/elements, such as :py:class:`Table` objects, support providing
multiple values to be clicked on in a single call.
action: a locator which will be clicked when the form filling is complete
action_always: if True, perform the action even if none of the
values to be filled in required any UI
interaction (eg, text boxes already had the
text to be filled in, checkbox already checked,
etc)
"""
logger.info('Beginning to fill in form...')
values = list(val for key in form.fields for val in values if val[0] == key[0])
res = []
for field, value in values:
if value is not None and form.field_valid(field):
loc = form.locators[field]
logger.trace(' Dispatching fill for "%s"' % field)
fill_prev = fill(loc, value) # re-dispatch to fill for each item
res.append(fill_prev != value) # note whether anything changed
elif value is None and isinstance(form.locators[field], Select):
fill_prev = fill(form.locators[field], None)
res.append(fill_prev != value)
else:
res.append(False)
if action and (any(res) or action_always): # only perform action if something changed
logger.debug(' Invoking end of form action')
fill(action, True) # re-dispatch with truthy value
logger.debug('Finished filling in form')
return any(res) or action_always
@fill.method((object, Mapping))
def _fill_form_dict(form, values, **kwargs):
"""Fill in a dict by converting it to a list"""
return _fill_form_list(form, values.items(), **kwargs)
[docs]class Radio(Input):
""" A class for Radio button groups
Radio allows the usage of HTML radio elements without resorting to previous
practice of iterating over elements to find the value. The name of the radio
group is passed and then when choices are required, the locator is built.
Args:
name: The HTML elements ``name`` attribute that identifies a group of radio
buttons.
Usage:
radio = Radio("schedule__schedule_type")
A specific radio element can then be returned by running the following::
el = radio.choice('immediately')
click(el)
The :py:class:`Radio` object can be reused over and over with repeated calls to
the :py:func:`Radio.choice` method.
"""
[docs] def choice(self, val):
""" Returns the locator for a choice
Args:
val: A string representing the ``value`` attribute of the specific radio
element.
Returns: A string containing the XPATH of the specific radio element.
"""
# Ugly, but working - all the conditions are in parentheses
return re.sub(r"\]$", " and @value={}]".format(quoteattr(val)), self.locate())
[docs] def observer_wait(self, val):
sel.detect_observed_field(self.choice(val))
@fill.method((Radio, object))
def _fill_radio(radio, value):
"""How to fill a radio button group (by selecting the given value)"""
logger.debug(' Filling in Radio{} with value "{}"'.format(tuple(radio.names), value))
sel.click(radio.choice(value))
radio.observer_wait(value)
[docs]class Tree(Pretty):
""" A class directed at CFME Tree elements
The Tree class aims to deal with all kinds of CFME trees, at time of writing there
are two distinct types. One which uses ``<table>`` elements and another which uses
``<ul>`` elements.
Args:
locator: This is a locator object pointing to either the outer ``<table>`` or
``<ul>`` element which contains the rest of the table.
Returns: A :py:class:`Tree` object.
A Tree object is set up by using a locator which contains the node elements. This element
will usually be a ``<ul>`` in the case of a Dynatree, or a ``<table>`` in the case of a
Legacy tree.
Usage:
tree = web_ui.Tree((By.XPATH, '//table//tr[@title="Datastore"]/../..'))
The path can then be navigated to return the last object in the path list, like so::
tree.click_path('Automation', 'VM Lifecycle Management (VMLifecycle)',
'VM Migrate (Migrate)')
Each path element will be expanded along the way, but will not be clicked.
When used in a :py:class:`Form`, a list of path tuples is expected in the form fill data.
The paths will be passed individually to :py:meth:`Tree.check_node`::
form = Form(fields=[
('tree_field', List(locator)),
])
form_fill_data = {
'tree_field': [
('Tree Node', 'Value'),
('Tree Node', 'Branch Node', 'Value'),
]
]
Note:
For legacy trees, the first element is often ignored as it is not a proper tree
element ie. in Automate->Explorer the Datastore element doesn't really exist, so we
omit it from the click map.
Legacy trees rely on a complex ``<table><tbody><tr><td>`` setup. We class a ``<tbody>``
as a node.
Note: Dynatrees, rely on a ``<ul><li>`` setup. We class a ``<li>`` as a node.
"""
pretty_attrs = ['locator']
def __init__(self, locator):
self.locator = locator
def _get_tag(self):
if getattr(self, 'tag', None) is None:
self.tag = sel.tag(sel.element(self.locator))
return self.tag
def _detect(self):
""" Detects which type of tree is being used
On invocation, first determines which type of Tree object it is dealing
with and then sets the internal variables to match elements of the specific tree class.
There are currently 4 attributes needed in the tree classes.
* expandable: the element to check if the tree is expanded/collapsed.
* is_expanded_condition: a tuple containing the element attribute and value to
identify that an element **is** expanded.
* node_label: an XPATH which describes a node's label (the element with just the text,
not including the expand arrow, etc), needing expansion with format specifier for
matching.
* node_root: XPATH expression for the entire node (including the expand arrow etc)
* click_expand: the element to click on to expand the tree at that level.
"""
self.root_el = sel.element(self.locator)
if self._get_tag() == 'ul':
# Dynatree
self.expandable = './span'
self.is_expanded_condition = ('class', 'dynatree-expanded', 'dynatree-has-children')
self.node_root = ".//li[span/a[normalize-space(.)='%s']]"
self.node_label = ".//li/span/a[normalize-space(.)='%s']"
self.click_expand = "./span/span"
self.leaf = "./span/a"
# Locators for reading the tree
# Finds all child nodes
self.nodes_root = "./li[span/a[@class='dynatree-title']]"
# How to get from the node to the container of child nodes
self.nodes_root_continue = "./ul"
# Label locator
self.node_label_loc = "./span/a[@class='dynatree-title']"
elif self._get_tag() == 'table':
# Legacy Tree
self.expandable = 'tr/td[1]/img'
self.is_expanded_condition = ('src', 'open.png')
self.node_root = ".//span[normalize-space(.)='%s']/../../.."
self.node_label = ".//span[normalize-space(.)='%s']"
self.click_expand = "tr/td[1]/img"
self.leaf = "tr/td/span"
# Locators for reading the tree - we do not support, this kind of getting, we have cust.
self.nodes_root = None
self.nodes_root_continue = None
self.node_label_loc = None
else:
raise exceptions.TreeTypeUnknown(
'The locator described does not point to a known tree type')
def _is_expanded(self, el):
""" Checks to see if an element is expanded
Args:
el: The element to check.
Returns: ``True`` if the element is expanded, ``False`` if not.
"""
try:
meta = sel.element(self.expandable, root=el)
except NoSuchElementException:
return True # Some trees have always-expanded roots
# This is a condition of checking whether this even is expandable
if len(self.is_expanded_condition) == 3:
if self.is_expanded_condition[2] not in sel.get_attribute(
meta, self.is_expanded_condition[0]):
return True # no need to expand
# It should be expandable, so now just check
if self.is_expanded_condition[1] in sel.get_attribute(
meta, self.is_expanded_condition[0]):
return True
else:
return False
def _expand(self, el, state=True):
""" Expands a tree node
Checks if a tree node needs expanding and then expands it.
Args:
el: The element to expand.
"""
if self._is_expanded(el) != state:
sel.click(sel.element(self.click_expand, root=el))
return True
else:
return False
[docs] def node_element(self, node_name, parent):
return sel.element((self.node_label % node_name), root=parent)
[docs] def node_root_element(self, node_name, parent):
return sel.element((self.node_root % node_name), root=parent)
[docs] def nodes_root_elements(self, parent):
return sel.elements(self.nodes_root, root=parent)
[docs] def expand_path(self, *path):
""" Clicks through a series of elements in a path.
Clicks through a tree, by expanding the levels in a single straight path and
returns the final element without clicking it.
Args:
*path: The path as multiple positional string arguments denoting the course to take.
Returns: The element at the leaf of the tree.
Raises:
cfme.exceptions.CandidateNotFound: A candidate in the tree could not be found to
continue down the path.
cfme.exceptions.TreeTypeUnknown: A locator was passed to the constructor which
does not correspond to a known tree type.
"""
# The detect here is required every time to avoid a StaleElementException if the
# Tree goes off screen and returns.
self._detect()
parent = self.locator
path = list(path)
node = None
for i, item in enumerate(path):
try:
node = self.node_root_element(item, parent)
except sel_exceptions.NoSuchElementException as e:
raise exceptions.CandidateNotFound(
{'message': "%s: could not be found in the tree." % item,
'path': path,
'index': i,
'cause': e})
self._expand(node)
parent = node
return node
[docs] def read_contents(self, parent=None, unexpand=False):
"""Reads complete contents of the tree recursively.
Tree is represented as a list. If the item in the list is string, it is leaf element and it
is its name. If the item is a tuple, first element of the tuple is the name and second
element is the subtree (list).
Args:
parent: Starting element, used during recursion
unexpand: Whether it should unexpand the expanded levels to original state.
Returns: Tree in format mentioned in description
"""
self._detect()
if parent is None and self._get_tag() == "table":
return self._legacy_read_contents() # Legacy
parent = self.locator if parent is None else parent
result = []
for item in self.nodes_root_elements(parent):
item_name = sel.text(self.node_label_loc, root=item).encode("utf-8").strip()
expanded = self._expand(item, True)
try:
item_contents = self.read_contents(
sel.element(self.nodes_root_continue, root=item), unexpand)
if item_contents is None:
result.append(item_name)
else:
result.append((item_name, item_contents))
if expanded and unexpand:
self._expand(item, False)
except NoSuchElementException:
result.append(item_name)
return result if len(result) > 0 else None
def _legacy_read_contents(self):
self._detect()
entry = sel.element(".//tbody[not(tr/td[contains(@class, 'hiddenRow')])]",
root=self.locator)
def _process_subtree(entry):
node_title = sel.text("./tr/td[@class='standartTreeRow']/span", root=entry)
self._expand(entry)
child_nodes = sel.elements("./tr[not(@title)]/td/table/tbody", root=entry)
if not child_nodes:
return node_title
else:
return (node_title, map(lambda tree: _process_subtree(tree), child_nodes))
return [_process_subtree(entry)]
[docs] def click_path(self, *path):
""" Exposes a path and then clicks it.
Args:
*path: The path as multiple positional string arguments denoting the course to take.
Returns: The leaf web element.
"""
# expand all but the last item
leaf = self.expand_path(*path[:-1]) or sel.element(self.locator)
if leaf:
try:
sel.click(self.node_element(path[-1], leaf))
except sel_exceptions.NoSuchElementException as e:
raise exceptions.CandidateNotFound(
{'message': "%s: could not be found in the tree." % path[-1],
'path': path,
'index': len(path) - 1,
'cause': e})
return leaf
@classmethod
[docs] def browse(cls, tree, *path):
"""Browse through tree via path.
If node not found, raises exception.
If the browsing reached leaf(str), returns True if also the step was last, otherwise False.
If the result of the path is a subtree, it is returned.
Args:
tree: List with tree.
*path: Path to browse.
"""
current = tree
for i, step in enumerate(path, start=1):
for node in current:
if isinstance(node, tuple):
if node[0] == step:
current = node[1]
break
else:
if node == step:
return i == len(path)
else:
raise Exception("Could not find node {}".format(step))
return current
@classmethod
[docs] def flatten_level(cls, tree):
"""Extracts just node names from current tree (top).
It makes:
.. code-block:: python
["asd", "fgh", ("ijk", [...]), ("lmn", [...])]
to
.. code-block:: python
["asd", "fgh", "ijk", "lmn"]
Useful for checking of contents of current tree level
"""
return map(lambda item: item[0] if isinstance(item, tuple) else item, tree)
[docs] def find_path_to(self, target):
""" Method used to look up the exact path to an item we know only by its regexp or partial
description.
Expands whole tree during the execution.
Args:
target: Item searched for. Can be regexp made by
:py:func:`re.compile <python:re.compile>`,
otherwise it is taken as a string for `in` matching.
Returns: :py:class:`list` with path to that item.
"""
if not isinstance(target, re._pattern_type):
target = re.compile(r".*?{}.*?".format(re.escape(str(target))))
def _find_in_tree(t, p=None):
if p is None:
p = []
for item in t:
if isinstance(item, tuple):
if target.match(item[0]) is None:
subtree = _find_in_tree(item[1], p + [item[0]])
if subtree is not None:
return subtree
else:
return p + [item[0]]
else:
if target.match(item) is not None:
return p + [item]
else:
return None
result = _find_in_tree(self.read_contents())
if result is None:
raise NameError("{} not found in tree".format(target.pattern))
else:
return result
[docs]class CheckboxTree(Tree):
"""Tree that has a checkbox on each node, adds methods to check/uncheck them"""
def _is_legacy_checked(self, leaf):
checkbox = sel.element(self.node_checkbox, root=leaf)
src = sel.get_attribute(checkbox, 'src')
for on_off, imgattrs in self.node_images.items():
for imgattr in imgattrs:
if imgattr in src:
return on_off
raise LookupError("Could not determine if Tree checkbox %s was checked or not"
% checkbox)
def _is_dynatree_checked(self, leaf):
return 'dynatree-selected' in \
sel.get_attribute(sel.element("span", root=leaf), 'class')
def _detect(self):
super(CheckboxTree, self)._detect()
if self._get_tag() == 'ul':
self.node_checkbox = "span/span[@class='dynatree-checkbox']"
self._is_checked = self._is_dynatree_checked
elif self._get_tag() == 'table':
self.node_checkbox = "tr/td[2]/img"
self.node_images = {True: ['iconCheckAll', 'radio_on'],
False: ['iconUncheckAll', 'radio_off']}
self._is_checked = self._is_legacy_checked
def _check_uncheck_node(self, path, check=False):
""" Checks or unchecks a node.
Args:
*path: The path as multiple positional string arguments denoting the course to take.
check: If ``True``, the node is checked, ``False`` the node is unchecked.
"""
self._detect()
leaf = self.expand_path(*path)
cb = sel.element(self.node_checkbox, root=leaf)
if check is not self._is_checked(leaf):
sel.click(cb)
[docs] def check_node(self, *path):
""" Convenience function to check a node
Args:
*path: The path as multiple positional string arguments denoting the course to take.
"""
self._check_uncheck_node(path, check=True)
[docs] def uncheck_node(self, *path):
""" Convenience function to uncheck a node
Args:
*path: The path as multiple positional string arguments denoting the course to take.
"""
self._check_uncheck_node(path, check=False)
@fill.method((Tree, Sequence))
def _fill_tree_seq(tree, values):
tree.click_path(*values)
@sel.select.method((CheckboxTree, Sequence))
@fill.method((CheckboxTree, Sequence))
def _select_chkboxtree_seq(cbtree, values):
"""values should be a list of tuple pairs, where the first item is the
path to select, and the second is whether to check or uncheck.
Usage:
select(cbtree, [(['Foo', 'Bar'], False),
(['Baz'], True)])
"""
for (path, to_select) in values:
if to_select:
cbtree.check_node(*path)
else:
cbtree.uncheck_node(*path)
[docs]class InfoBlock(Pretty):
DETAIL = "detail"
FORM = "form"
_TITLE_CACHE = {}
pretty_attrs = ["title"]
def __new__(cls, title, detail=None):
# Caching
if title not in cls._TITLE_CACHE:
cls._TITLE_CACHE[title] = super(InfoBlock, cls).__new__(cls)
cls._TITLE_CACHE[title].__init__(title)
instance = cls._TITLE_CACHE[title]
if detail is None:
return instance
else:
return instance.member(detail)
def __init__(self, title):
if all(map(lambda a: hasattr(self, a), ["title", "_type", "_member_cache"])):
return
self.title = title
self._type = None
self._member_cache = {}
@property
[docs] def type(self):
if self._type is None:
self.root # To retrieve it
return self._type
@property
[docs] def root(self):
possible_locators = [
# Detail type
version.pick({
'5.3': '//table//th[contains(normalize-space(.), "{}")]/../../../..'.format(
self.title),
version.LOWEST:
'//div[@class="modbox"]/h2[@class="modtitle"]'
'[contains(normalize-space(.), "{}")]/..'.format(self.title)
}),
# Form type
(
'//*[p[@class="legend"][contains(normalize-space(.), "{}")] and table/tbody/tr/td['
'contains(@class, "key")]]'.format(self.title)
),
# Newer Form type (master.20150311020845_547fd06 onwards)
(
'//*[h3[contains(normalize-space(.), "{}")] and table/tbody/tr/td['
'contains(@class, "key")]]'.format(self.title)
)
# The root element must contain table element because listaccordions were caught by the
# locator. It used to be fieldset but it seems it can be really anything
]
found = sel.elements("|".join(possible_locators))
if not found:
raise exceptions.BlockTypeUnknown("The block type requested is unknown")
root_el = found[0]
if sel.elements("./table/tbody/tr/td[contains(@class, 'key')]", root=root_el):
self._type = self.FORM
else:
self._type = self.DETAIL
return root_el
[docs] def member(self, name):
if name not in self._member_cache:
self._member_cache[name] = self.Member(self, name)
return self._member_cache[name]
[docs] def by_member_icon(self, icon):
"""In case you want to find the item by icon in the value field (like OS infra diff.)"""
l = ".//table/tbody/tr/td[2]/img[contains(@src, {})]/../../td[1]".format(quoteattr(icon))
return self.member(sel.text(l))
def __call__(self, member):
"""A present for @smyers"""
return self.member(member)
##
#
# Shortcuts for old-style access
#
@classmethod
[docs] def text(cls, *args, **kwargs):
return cls(*args, **kwargs).text
@classmethod
[docs] def element(cls, *args, **kwargs):
return cls(*args, **kwargs).element
@classmethod
[docs] def elements(cls, *args, **kwargs):
return cls(*args, **kwargs).elements
@classmethod
[docs] def icon_href(cls, *args, **kwargs):
return cls(*args, **kwargs).icon_href
@classmethod
[docs] def container(cls, args, **kwargs):
try:
return sel.element(cls(*args, **kwargs).container)
except sel_exceptions.NoSuchElementException:
raise exceptions.ElementOrBlockNotFound(
"Either the element of the block could not be found")
[docs] class Member(Pretty):
pretty_attrs = "name", "ib"
def __init__(self, ib, name):
self.ib = ib
self.name = name
@property
[docs] def pair_locator(self):
if self.ib.type == InfoBlock.DETAIL:
return './/table/tbody/tr/td[1][@class="label"][normalize-space(.)="{}"]/..'.format(
self.name)
elif self.ib.type == InfoBlock.FORM:
return './/table/tbody/tr/td[1][@class="key"][normalize-space(.)="{}"]/..'.format(
self.name)
@property
[docs] def pair(self):
return sel.element(self.pair_locator, root=self.ib.root)
@property
[docs] def container(self):
return sel.element("./td[2]", root=self.pair)
[docs] def locate(self):
return self.container
@property
[docs] def elements(self):
return sel.elements("./*", root=self.container)
@property
[docs] def element(self):
return self.elements[0]
@property
[docs] def text(self):
return sel.text(self.container).encode("utf-8").strip()
@property
[docs] def icon_href(self):
try:
return sel.get_attribute(sel.element("./img", root=self.container), "src")
except sel_exceptions.NoSuchElementException:
return None
@property
[docs] def title(self):
return sel.get_attribute(self.pair, "title") or None
@fill.method((InfoBlock, Sequence))
def _ib_seq(ib, i):
for item in i:
sel.click(ib.member(item))
@fill.method((InfoBlock, basestring))
def _ib_str(ib, s):
fill([s])
@fill.method((InfoBlock.Member, bool))
def _ib_m_seq(member, b):
if b:
sel.click(member)
[docs]class Quadicon(Pretty):
"""
Represents a single quadruple icon in the CFME UI.
A Quadicon contains multiple quadrants. These are accessed via attributes.
The qtype is currently one of the following and determines which attribute names
are present. They are mapped internally and can be reassigned easily if the UI changes.
A Quadicon is used by defining the name of the icon and the type. After that, it can be used
to obtain the locator of the Quadicon, or query its quadrants, via attributes.
Args:
name: The label of the icon.
qtype: The type of the quad icon. By default it is ``None``, therefore plain quad without any
retrievable data usable for selecting/clicking.
Usage:
qi = web_ui.Quadicon('hostname.local', 'host')
qi.creds
click(qi)
.. rubric:: Known Quadicon Types and Attributes
* **host** - *from the infra/host page* - has quads:
* a. **no_vm** - Number of VMs
* b. **state** - The current state of the host
* c. **vendor** - The vendor of the host
* d. **creds** - If the creds are valid
* **infra_prov** - *from the infra/providers page* - has quads:
* a. **no_host** - Number of hosts
* b. *Blank*
* c. **vendor** - The vendor of the provider
* d. **creds** - If the creds are valid
* **vm** - *from the infra/virtual_machines page* - has quads:
* a. **os** - The OS of the vm
* b. **state** - The current state of the vm
* c. **vendor** - The vendor of the vm's host
* d. **no_snapshot** - The number of snapshots
* g. **policy** - The state of the policy
* **cloud_prov** - *from the cloud/providers page* - has quads:
* a. **no_instance** - Number of instances
* b. **no_image** - Number of machine images
* c. **vendor** - The vendor of the provider
* d. **creds** - If the creds are valid
* **instance** - *from the cloud/instances page* - has quads:
* a. **os** - The OS of the instance
* b. **state** - The current state of the instance
* c. **vendor** - The vendor of the instance's host
* d. **no_snapshot** - The number of snapshots
* g. **policy** - The state of the policy
* **datastore** - *from the infra/datastores page* - has quads:
* a. **type** - File system type
* b. **no_vm** - Number of VMs
* c. **no_host** - Number of hosts
* d. **avail_space** - Available space
* **repository** - *from the infra/repositories page* - has no quads
* **cluster** - *from the infra/cluster page* - has no quads
* **resource_pool** - *from the infra/resource_pool page* - has no quads
* **stack** - *from the clouds/stacks page* - has no quads
Returns: A :py:class:`Quadicon` object.
"""
pretty_attrs = ['_name', '_qtype']
QUADS = {
"host": {
"no_vm": ("a", 'txt'),
"state": ("b", 'img'),
"vendor": ("c", 'img'),
"creds": ("d", 'img'),
},
"infra_prov": {
"no_host": ("a", 'txt'),
"vendor": ("c", 'img'),
"creds": ("d", 'img'),
},
"vm": {
"os": ("a", 'img'),
"state": ("b", 'img'),
"vendor": ("c", 'img'),
"no_snapshot": ("d", 'txt'),
"policy": ("g", 'img'),
},
"cloud_prov": {
"no_vm": ("a", 'txt'),
"no_image": ("b", 'txt'),
"vendor": ("b", 'img'),
"creds": ("d", 'img'),
},
"instance": {
"os": ("a", 'img'),
"state": ("b", 'img'),
"vendor": ("c", 'img'),
"no_snapshot": ("d", 'txt'),
"policy": ("g", 'img'),
},
"stack": {},
"datastore": {
"type": ("a", 'img'),
"no_vm": ("b", 'txt'),
"no_host": ("c", 'txt'),
"avail_space": ("d", 'img'),
},
"cluster": {},
"repository": {},
"resource_pool": {},
"template": {
"os": ("a", 'img'),
"state": ("b", 'img'),
"vendor": ("c", 'img'),
"no_snapshot": ("d", 'txt'),
},
"image": {
"os": ("a", 'img'),
"state": ("b", 'img'),
"vendor": ("c", 'img'),
"no_snapshot": ("d", 'txt'),
},
None: {}, # If you just want to find the quad and not mess with data
}
def __init__(self, name, qtype=None):
self._name = name
self.qtype = qtype
@property
def qtype(self):
return self._qtype
@qtype.setter
[docs] def qtype(self, value):
assert value in self.QUADS
self._qtype = value
@property
def _quad_data(self):
return self.QUADS[self.qtype]
[docs] def checkbox(self):
""" Returns: a locator for the internal checkbox for the quadicon"""
return "//input[@type='checkbox' and ../../..//a[@title={}]]".format(quoteattr(self._name))
@property
[docs] def exists(self):
try:
self.locate()
return True
except sel.NoSuchElementException:
return False
[docs] def locate(self):
""" Returns: a locator for the quadicon anchor"""
try:
return sel.move_to_element('div/a',
root="//div[@id='quadicon' and ../../..//a[@title={}]]".format(
quoteattr(self._name)))
except sel.NoSuchElementException:
quads = sel.elements("//div[@id='quadicon']/../../../tr/td/a")
if not quads:
raise sel.NoSuchElementException("Quadicon {} not found. No quads present".format(
self._name))
else:
quad_names = [sel.get_attribute(quad, "title") for quad in quads]
raise sel.NoSuchElementException(
"Quadicon {} not found. These quads are present:\n{}".format(
self._name, ", ".join(quad_names)))
def _locate_quadrant(self, corner):
""" Returns: a locator for the specific quadrant"""
return "//div[contains(@class, {}) and ../../../..//a[@title={}]]".format(
quoteattr("{}72".format(corner)),
quoteattr(self._name))
def __getattr__(self, name):
""" Queries the quadrants by name
Args:
name: The name of the quadrant identifier, as defined above.
Returns: A string containing a representation of what is in the quadrant.
"""
if name in self._quad_data:
corner, rtype = self._quad_data[name]
locator = self._locate_quadrant(corner)
# We have to have a try/except here as some quadrants
# do not exist if they have no data, e.g. current_state in a host
# with no credentials.
try:
el = sel.element(locator)
except sel_exceptions.NoSuchElementException:
return None
if rtype == 'txt':
return el.text
if rtype == 'img':
img_el = sel.element('.//img', root=el)
img_name = sel.get_attribute(img_el, 'src')
path, filename = os.path.split(img_name)
root, ext = os.path.splitext(filename)
return root
else:
return object.__getattribute__(self, name)
def __str__(self):
return self.locate()
@classmethod
[docs] def all(cls, qtype=None, this_page=False):
"""Allows iteration over Quadicons.
Args:
qtype: Quadicon type. Refer to the constructor for reference.
this_page: Whether to look for Quadicons only on current page (do not list pages).
Returns: :py:class:`list` of :py:class:`Quadicon`
"""
from cfme.web_ui import paginator # Prevent circular imports
if this_page:
pages = (None, ) # Single, current page. Since we dont care about the value, using None
else:
pages = paginator.pages()
for page in pages:
for href in sel.elements("//div[@id='quadicon']/../../../tr/td/a"):
yield cls(sel.get_attribute(href, "title"), qtype)
@classmethod
[docs] def first(cls, qtype=None):
return cls(cls.get_first_quad_title(), qtype=qtype)
@staticmethod
[docs] def select_first_quad():
fill("//div[@id='quadicon']/../..//input", True)
@staticmethod
[docs] def get_first_quad_title():
return sel.get_attribute("//div[@id='quadicon']/../../../tr/td/a", "title")
@classproperty
[docs] def any_present(cls):
try:
cls.get_first_quad_title()
except NoSuchElementException:
return False
except AttributeError:
# This is needed so that if there is no browser, we fail nicely, this in turn is
# needed to make the docs not error.
return False
else:
return True
@property
[docs] def name(self):
""" Returns name of the quadicon."""
return self._name
@property
[docs] def check_for_single_quadrant_icon(self):
""" Checks if the quad icon is a single quadrant icon."""
for quadrant_name in self._quad_data.iterkeys():
# These quadrant will be displayed if it is a regular quad
quadrant_id = self._quad_data[quadrant_name][0] # It is a tuple
if sel.is_displayed(self._locate_quadrant(quadrant_id)):
return False
return sel.is_displayed(self._locate_quadrant("e")) # Image has only 'e'
[docs]class DHTMLSelect(Select):
"""
A special Select object for CFME's icon enhanced DHTMLx Select elements.
Args:
loc: A locator.
Returns a :py:class:`cfme.web_ui.DHTMLSelect` object.
"""
@staticmethod
def _log(meth, val=None):
if val:
val_string = " with value %s" % val
logger.debug('Filling in DHTMLSelect using (%s)%s' % (meth, val_string))
def _get_select_name(self):
""" Get's the name reference of the element from its hidden attribute.
"""
root_el = sel.element(self)
el = sel.element("div/input[2]", root=root_el)
name = sel.get_attribute(el, 'name')
return name
@property
[docs] def all_selected_options(self):
""" Returns all selected options.
Note: Since the DHTML select can only have one option selected at a time, we
simple return the first element (the only element).
Returns: A Web element.
"""
return [self.first_selected_option]
@property
[docs] def first_selected_option(self):
""" Returns the first selected option in the DHTML select
Note: In a DHTML select, there is only one option selectable at a time.
Returns: A webelement.
"""
name = self._get_select_name()
return browser().execute_script(
'return %s.getOptionByIndex(%s.getSelectedIndex()).content' % (name, name))
@property
[docs] def options(self):
""" Returns a list of options of the select as webelements.
Returns: A list of Webelements.
"""
name = self._get_select_name()
return browser().execute_script('return %s.DOMlist.children' % name)
[docs] def select_by_index(self, index, _cascade=None):
""" Selects an option by index.
Args:
index: The select element's option by index.
"""
name = self._get_select_name()
if index is not None:
if not _cascade:
self._log('index', index)
browser().execute_script('%s.selectOption(%s)' % (name, index))
[docs] def select_by_visible_text(self, text):
""" Selects an option by visible text.
Args:
text: The select element option's visible text.
"""
name = self._get_select_name()
if text is not None:
self._log('visible_text', text)
value = browser().execute_script('return %s.getOptionByLabel("%s").value'
% (name, text))
self.select_by_value(value, _cascade=True)
[docs] def select_by_value(self, value, _cascade=None):
""" Selects an option by value.
Args:
value: The select element's option value.
"""
name = self._get_select_name()
if value is not None:
if not _cascade:
self._log('value', value)
index = browser().execute_script('return %s.getIndexByValue("%s")' % (name, value))
self.select_by_index(index, _cascade=True)
[docs] def locate(self):
return sel.move_to_element(self._loc)
@sel.select.method((DHTMLSelect, basestring))
[docs]def select_dhtml(dhtml, s):
dhtml.select_by_visible_text(s)
[docs]class Filter(Form):
""" Filters requests pages
This class inherits Form as its base and adds a few methods to assist in filtering
request pages.
Usage:
f = Filter(fields=[
('type', Select('//select[@id="type_choice"]')),
('approved', Input("state_choice__approved")),
('denied', Input"state_choice__denied")),
('pending_approval', Input("state_choice__pending_approval")),
('date', Select('//select[@id="time_period"]')),
('reason', Input("reason_text")),
])
f.apply_filter(type="VM Clone", approved=False,
pending_approval=False, date="Last 24 Hours", reason="Just Because")
"""
buttons = {
'default_off': '//div[@id="buttons_off"]/li/a/img[@alt="Set filters to default"]',
'default_on': '//div[@id="buttons_on"]/li/a/img[@alt="Set filters to default"]',
'apply': '//div[@id="buttons_on"]//a[@title="Apply the selected filters"]',
'reset': '//div[@id="buttons_on"]//a[@title="Reset filter changes"]'
}
[docs] def default_filter(self):
""" Method to reset the filter back to defaults.
"""
sel.click(self.buttons['default_off'])
sel.click(self.buttons['default_on'])
[docs] def reset_filter(self):
""" Method to reset the changes to the filter since last applying.
"""
sel.click(self.buttons['reset'])
[docs] def apply_filter(self, **kwargs):
""" Method to apply a filter.
First resets the filter to default and then applies the filter.
Args:
**kwargs: A dictionary of form elements to fill and their values.
"""
self.default_filter()
self.fill(kwargs)
sel.click(self.buttons['apply'])
[docs]class MultiSelect(Region):
"""Represents a UI widget where there are two select boxes, one with
possible selections, and another with selected items. Has two
arrow buttons to move items between the two"""
def __init__(self,
available_select=None,
selected_select=None,
select_arrow=None,
deselect_arrow=None):
self.available_select = available_select
self.selected_select = selected_select
self.select_arrow = select_arrow
self.deselect_arrow = deselect_arrow
@sel.select.method((MultiSelect, Sequence))
[docs]def select_multiselect(ms, values):
sel.select(ms.available_select, values)
sel.click(ms.select_arrow)
@fill.method((MultiSelect, Sequence))
[docs]def fill_multiselect(ms, items):
sel.select(ms, items)
[docs]class UpDownSelect(Region):
"""Multiselect with two arrows (up/down) next to it. Eg. in AE/Domain priority selection.
Args:
select_loc: Locator for the select box (without Select element wrapping)
up_loc: Locator of the Move Up arrow.
down_loc: Locator with Move Down arrow.
"""
def __init__(self, select_loc, up_loc, down_loc):
super(UpDownSelect, self).__init__(locators=dict(
select=Select(select_loc, multi=True),
up=up_loc,
down=down_loc,
))
[docs] def get_items(self):
return map(lambda el: el.text.encode("utf-8"), self.select.options)
[docs] def move_up(self, item):
item = str(item)
assert item in self.get_items()
self.select.deselect_all()
sel.select(self.select, item)
sel.click(self.up)
[docs] def move_down(self, item):
item = str(item)
assert item in self.get_items()
self.select.deselect_all()
sel.select(self.select, item)
sel.click(self.down)
[docs] def move_top(self, item):
item = str(item)
assert item in self.get_items()
self.select.deselect_all()
while item != self.get_items()[0]:
sel.select(self.select, item)
sel.click(self.up)
[docs] def move_bottom(self, item):
item = str(item)
assert item in self.get_items()
self.select.deselect_all()
while item != self.get_items()[-1]:
sel.select(self.select, item)
sel.click(self.down)
@fill.method((UpDownSelect, Sequence))
def _fill_uds_seq(uds, seq):
seq = map(str, seq)
for item in reversed(seq): # reversed because every new item at top pushes others down
uds.move_top(item)
[docs]class ScriptBox(Pretty):
"""Represents a script box as is present on the customization templates pages.
This box has to be activated before keys can be sent. Since this can't be done
until the box element is visible, and some dropdowns change the element, it must
be activated "inline".
Args:
"""
pretty_attrs = ['locator']
def __init__(self, name=None, ta_locator="//textarea[contains(@id, 'method_data')]"):
self._name = name
self.ta_loc = ta_locator
@property
[docs] def name(self):
if not self._name:
self._name = version.pick({
version.LOWEST: 'miqEditor',
'5.5': 'ManageIQ.editor'})
return self._name
[docs] def get_value(self):
script = sel.execute_script('return {}.getValue();'.format(self.name))
script = script.replace('\\"', '"').replace("\\n", "\n")
return script
@fill.method((ScriptBox, Anything))
[docs]def fill_scriptbox(sb, script):
"""This function now clears and sets the ScriptBox.
"""
script = script.replace('"', '\\"').replace("\n", "\\n")
sel.execute_script('{}.setValue("{}");'.format(sb.name, script))
sel.wait_for_ajax()
sel.execute_script('{}.save();'.format(sb.name))
sel.wait_for_ajax()
@fill.method((EmailSelectForm, basestring))
@fill.method((EmailSelectForm, list))
@fill.method((EmailSelectForm, set))
@fill.method((EmailSelectForm, tuple))
[docs]class CheckboxSelect(Pretty):
"""Class used for filling those bunches of checkboxes I (@mfalesni) always hated to search for.
Can fill by values, text or both. To search the text for the checkbox, you have 2 choices:
* If the text can be got from parent's tag (like `<div><input type="checkbox">blablabla</div>`
where blablabla is the checkbox's description looked up), you can leave the
`text_access_func` unfilled.
* If there is more complicated layout and you don't mind a bit slower operation, you can pass
the text_access_func, which should be like `lambda checkbox_el: get_text_of(checkbox_el)`.
The checkbox `WebElement` is passed to it and the description text is the expected output
of the function.
Args:
search_root: Root element for checkbox search
text_access_func: Function returning descriptive text about passed CB element.
"""
pretty_attrs = ['_root']
def __init__(self, search_root, text_access_func=None):
self._root = search_root
self._access_func = text_access_func
@property
[docs] def checkboxes(self):
"""All checkboxes."""
return set(sel.elements(".//input[@type='checkbox']", root=sel.element(self._root)))
@property
[docs] def selected_checkboxes(self):
"""Only selected checkboxes."""
return {cb for cb in self.checkboxes if cb.is_selected()}
@property
[docs] def selected_values(self):
"""Only selected checkboxes' values."""
return {sel.get_attribute(cb, "value") for cb in self.selected_checkboxes}
@property
[docs] def unselected_checkboxes(self):
"""Only unselected checkboxes."""
return {cb for cb in self.checkboxes if not cb.is_selected()}
@property
[docs] def unselected_values(self):
"""Only unselected checkboxes' values."""
return {sel.get_attribute(cb, "value") for cb in self.unselected_checkboxes}
[docs] def checkbox_by_id(self, id):
"""Find checkbox's WebElement by id."""
return sel.element(
".//input[@type='checkbox' and @id='%s']" % id, root=sel.element(self._root)
)
[docs] def select_all(self):
"""Selects all checkboxes."""
for cb in self.unselected_checkboxes:
sel.check(cb)
[docs] def unselect_all(self):
"""Unselects all checkboxes."""
for cb in self.selected_checkboxes:
sel.uncheck(cb)
[docs] def checkbox_by_text(self, text):
"""Returns checkbox's WebElement by searched by its text."""
if self._access_func is not None:
for cb in self.checkboxes:
txt = self._access_func(cb)
if txt == text:
return cb
else:
raise NameError("Checkbox with text %s not found!" % text)
else:
# Has to be only single
return sel.element(
".//*[contains(., '%s')]/input[@type='checkbox']" % text,
root=sel.element(self._root)
)
[docs] def check(self, values):
"""Checking function.
Args:
values: Dictionary with key=CB name, value=bool with status.
Look in the function to see.
"""
for name, value in values.iteritems():
if isinstance(name, sel.ByText):
sel.checkbox(self.checkbox_by_text(str(name)), value)
else:
sel.checkbox(self.checkbox_by_id(name), value)
@fill.method((CheckboxSelect, bool))
[docs]def fill_cb_select_bool(select, all_state):
if all_state is True:
return select.select_all()
else:
return select.unselect_all()
@fill.method((CheckboxSelect, list))
@fill.method((CheckboxSelect, set))
[docs]def fill_cb_select_set(select, names):
return select.check({k: True for k in names})
@fill.method((CheckboxSelect, Mapping))
[docs]def fill_cb_select_dictlist(select, dictlist):
return select.check(dictlist)
@fill.method((CheckboxSelect, basestring))
@fill.method((CheckboxSelect, sel.ByText))
[docs]def fill_cb_select_string(select, cb):
return fill(select, {cb})
@fill.method((ShowingInputs, Sequence))
def _fill_showing_inputs_seq(si, i):
for loc, val in si.zip(i):
fill(loc, val)
@fill.method((ShowingInputs, basestring))
def _fill_showing_inputs_str(si, s):
fill(si, [s])
[docs]class Timelines(Pretty):
"""
A Timelines object represents the Timelines widget in CFME
Args:
loc: A locator for the Timelines element, usually the div with
id miq_timeline.
"""
pretty_attrs = ['element']
[docs] class Object(Pretty):
"""
A generic timelines object.
Args:
element: A WebElement for the event.
"""
pretty_attrs = ['element']
def __init__(self, element):
self.element = element
self.pos = self.element.value_of_css_property('left')
self.text = self.element.text
[docs] def locate(self):
return self.element
[docs] class Event(Object):
"""
An event object.
"""
window_loc = '//div[@class="timeline-event-bubble-title"]/../..'
close_button = "{}/div[contains(@style, 'close-button')]".format(window_loc)
data_block = '{}//div[@class="timeline-event-bubble-body"]'.format(window_loc)
@property
[docs] def image(self):
""" Returns the image name of an event. """
el = sel.element('.//img', root=self.element)
if el:
return os.path.split(sel.get_attribute(el, 'src'))[1]
return False
[docs] def open_block(self):
""" Opens the events info block. """
self.close_block()
sel.click(self.element)
[docs] def close_block(self):
""" Closes the events info block. """
try:
sel.click(self.close_button)
except (NoSuchElementException, MoveTargetOutOfBoundsException):
pass
[docs] def block_info(self):
""" Attempts to return a dict with the information from the popup. """
self.open_block()
data = {}
elem = sel.element(self.data_block)
text_elements = elem.text.split("\n")
for line in text_elements:
line += " "
kv = line.split(": ")
if len(kv) == 1:
if ':' not in kv[0]:
data['title'] = kv[0].strip()
else:
data[kv[0]] = None
else:
data[kv[0]] = kv[1].strip()
return data
[docs] class Marker(Object):
""" A proxied object in case it needs more methods further down the line."""
pass
def __init__(self, loc):
self.loc = loc
def _list_events(self):
ele = sel.elements('.//div[@name="events"]/div', root=self.loc)
return ele
def _list_markers(self):
ele = sel.elements('.//div[@name="ether-markers"]/div', root=self.loc)
return ele
[docs] def find_first_marker_in_range(self):
""" Finds the first marker on screen. """
for marker in self.markers():
if sel.is_displayed(marker.element):
return marker
[docs] def find_first_event_in_range(self):
""" Finds the first event on screen. """
marker = self.find_first_marker_in_range()
pos = marker.pos
for event in self.events():
if event.pos > pos:
return event
[docs] def visible_events(self):
""" A generator giving all visible events. """
marker = self.find_first_marker_in_range()
pos = marker.pos
for event in self.events():
if event.pos > pos:
yield event
[docs] def find_visible_events_for_vm(self, vm_name):
""" Finds all events for a given vm.
Args:
vm_name: The vm name.
"""
events = []
for event in self.visible_events():
info = event.block_info()
if info.get('title', None) == vm_name:
events.append(event)
event.close_block()
return events
[docs] def events(self):
""" A generator yielding all events. """
for el in self._list_events():
yield self.Event(el)
[docs] def markers(self):
""" A generator yielding all markers. """
for el in self._list_markers():
yield self.Marker(el)
[docs]class MultiFill(object):
"""Class designed to fill the same value to multiple fields
Args:
*fields: The fields where the value will be mirrored
"""
def __init__(self, *fields):
self.fields = fields
@fill.method((MultiFill, object))
def _fill_multi_obj(mf, o):
for field in mf.fields:
fill(field, o)
[docs]class DriftGrid(Pretty):
""" Class representing the table (grid) specific to host drift analysis comparison page
"""
def __init__(self, loc="//div[@id='drift_grid_div']"):
self.loc = loc
[docs] def get_cell(self, row_text, col_index):
""" Finds cell element of the grid specified by column index and row text
Args:
row_text: Title text of the cell's row
col_index: Column index of the cell, starting with 0 for 1st data-containing column
Note:
`col_index` of 0 is used for the 2nd actual column in the drift grid, because
the 1st column does not contain headers, only row descriptions.
Returns:
Selenium element of the cell.
"""
self.expand_all_sections()
cell_loc = {
'5.3': ".//div/div[1][contains(., '{}')]/../div[{}]".format(row_text, col_index + 2),
version.LOWEST: ".//tr/td[1][contains(., '{}')]/../td[{}]"
.format(row_text, col_index + 2)
}
cell = sel.element(cell_loc, root=self.loc)
return cell
[docs] def cell_indicates_change(self, row_text, col_index):
""" Finds out if a cell, specified by column index and row text, indicates change
Args:
row_text: Title text of the cell's row
col_index: Column index of the cell
Note:
`col_index` of 0 is used for the 2nd actual column in the drift grid, because
the 1st column does not contain headers, only row descriptions.
Returns:
``True`` if there is a change present, ``False`` otherwise
"""
cell = self.get_cell(row_text, col_index)
# Cell either contains an image
try:
cell_img = sel.element(".//img", root=cell)
if sel.get_attribute(cell_img, "alt") == 'Changed from previous':
return True
# or text
except NoSuchElementException:
if version.current_version() <= '5.3':
cell_textdiv = sel.element("./div", root=cell)
if 'mark' in sel.get_attribute(cell_textdiv, 'class'):
return True
else: # LOWEST
if 'color: rgb(33, 160, 236)' in sel.get_attribute(cell, 'style'):
return True
return False
[docs] def expand_all_sections(self):
""" Expands all sections to make the row elements found therein available
"""
while True:
# We need to do this one by one because the DOM changes on every expansion
try:
el = sel.element({
'5.3': './/div/span[contains(@class, "toggle") and contains(@class, "expand")]',
version.LOWEST: './/div/img[contains(@src, "plus")]'},
root=self.loc)
sel.click(el)
except NoSuchElementException:
break
@fill.method((ButtonGroup, basestring))
def _fill_showing_button_group(tb, s):
tb.choose(s)
[docs]class ColorGroup(object):
def __init__(self, key):
""" A ColourGroup is a set of colour buttons next to each other, as is used on the DefaultViews
page.
Args:
key: The name of the key field text before the button group.
"""
self.key = key
self.locator = '//td[@class="key" and text()="{}"]/..'.format(self.key)
[docs] def locate(self):
""" Moves to the element """
# Use the header locator as the overall table locator
return sel.move_to_element(self.locator)
@property
[docs] def active(self):
""" Returns the alt tag text of the active button in thr group. """
loc = sel.element(self.locator + '/td[2]/div[contains(@title, "selected")]')
color = re.search('The (.*?) theme', loc.get_attribute('title')).groups()[0]
return color
[docs] def status(self, color):
""" Returns the status of the color button identified by the Title Text of the image. """
active_loc = self.locator + '/td[2]/div[contains(@title, "{}")' \
'and contains(@title, "selected")]'.format(color)
try:
sel.element(active_loc)
return True
except NoSuchElementException:
pass
inactive_loc = self.locator + '/td[2]/div[contains(@title, "{}")' \
'and contains(@title, "Click")]'.format(color)
try:
sel.element(inactive_loc)
return False
except NoSuchElementException:
pass
[docs] def choose(self, color):
""" Sets the ColorGroup to select the button identified by the title text. """
if not self.status(color):
inactive_loc = self.locator + '/td[2]/div[contains(@title, "{}")' \
'and contains(@title, "Click")]'.format(color)
sel.click(inactive_loc)
@fill.method((ColorGroup, basestring))
def _fill_showing_color_group(tb, s):
tb.choose(s)
[docs]class DynamicTable(Pretty):
"""A table that can add or remove the rows.
"""
pretty_attrs = "root_loc", "default_row_item"
ROWS = ".//tbody/tr[not(contains(@id, 'new_tr'))]"
def __init__(self, root_loc, default_row_item=None):
self.root_loc = root_loc
self.default_row_item = default_row_item
@property
[docs] def rows(self):
return map(lambda r_el: self.Row(self, r_el), sel.elements(self.ROWS, root=self.root_loc))
@lazycache
[docs] def click_add(self):
sel.click(sel.element(".//tbody/tr[@id='new_tr']/td//img", root=self.root_loc))
[docs] def click_save(self):
sel.click(sel.element(
".//tbody/tr[@id='new_tr']/td//input[@type='image']", root=self.root_loc))
[docs] def delete_row(self, by):
pass
[docs] def clear(self):
while True:
buttons = sel.elements(".//tbody/tr/td/img[@alt='Delete']")
if not buttons:
break
sel.click(buttons[0])
[docs] def add_row(self, data):
self.click_add()
editing_row = self.Row(self, ".//tbody/tr[@id='new_tr']")
fill(editing_row, data)
self.click_save()
[docs] class Row(object):
def __init__(self, table, root):
self.table = table
self.root = root
@property
[docs] def values(self):
cells = sel.elements("./td", root=self.root)
return dict(zip(self.table.header_names, map(sel.text, cells)))
@property
@property
@fill.method((DynamicTable.Row, Mapping))
def _fill_dt_row_map(dtr, m):
for name, input in dtr.inputs_for_filling.iteritems():
fill(input, m.get(name, None))
@fill.method((DynamicTable.Row, Anything))
def _fill_dt_row_other(dtr, anything):
mapping_fields = [name for name in dtr.table.header_names if name.strip()]
if isinstance(anything, (list, tuple)) and len(anything) == len(mapping_fields):
# Create the dict and fill by dict
fill(dtr, dict(zip(mapping_fields, anything)))
else:
# Use the default field
if dtr.table.default_row_item is None:
raise Exception("Cannot fill table row with anything when we dont know the def. field")
fill(dtr, {dtr.table.default_row_item: anything})
@fill.method((DynamicTable, list))
def _fill_dt_list(dt, l, clear_before=False):
if clear_before:
dt.clear()
for item in l:
dt.add_row(item)
@fill.method((DynamicTable, Anything))
def _fill_dt_anything(dt, anything, **kwargs):
fill(dt, [anything], **kwargs)
fill.prefer((DynamicTable, Anything), (object, Mapping))
fill.prefer((DynamicTable.Row, Anything), (object, Mapping))
fill.prefer((Select, types.NoneType), (object, types.NoneType))
fill.prefer((DHTMLSelect, types.NoneType), (object, types.NoneType))
fill.prefer((object, types.NoneType), (Select, object))
[docs]class AngularSelect(object):
BUTTON = "//button[@data-id='{}']"
def __init__(self, loc, none=None, multi=False):
self.none = none
if isinstance(loc, AngularSelect):
self._loc = loc._loc
else:
self._loc = self.BUTTON.format(loc)
self.multi = multi
[docs] def locate(self):
return sel.move_to_element(self._loc)
@property
[docs] def select(self):
return Select('select#{}'.format(self.did), multi=self.multi)
@property
[docs] def did(self):
return sel.element(self._loc).get_attribute('data-id')
@property
[docs] def is_open(self):
el = sel.element(self._loc)
return el.get_attribute('aria-expanded') == "true"
[docs] def open(self):
el = sel.element(self._loc)
el.click()
[docs] def select_by_visible_text(self, text):
if not self.is_open:
self.open()
new_loc = self._loc + '/../div/ul/li/a[contains(., "{}")]'.format(text)
e = sel.element(new_loc)
sel.execute_script("arguments[0].scrollIntoView();", e)
sel.click(new_loc)
[docs] def select_by_value(self, value):
value = str(value) # Because what we read from the page is a string
options_map = [a.value for a in self.select.all_options]
index = options_map.index(value)
if not self.is_open:
self.open()
new_loc = self._loc + '/../div/ul/li[@data-original-index={}]'.format(index)
e = sel.element(new_loc)
sel.execute_script("arguments[0].scrollIntoView();", e)
sel.click(new_loc)
@property
[docs] def all_options(self):
return self.select.all_options
@property
[docs] def classes(self):
"""Combines class from the button and from select."""
return sel.classes(self) | sel.classes("select#{}".format(self.did))
@property
[docs] def options(self):
return self.select.options
@property
[docs] def first_selected_option(self):
return self.select.first_selected_option
@property
[docs] def first_selected_option_text(self):
return self.select.first_selected_option_text
@fill.method((AngularSelect, sel.ByText))
@fill.method((AngularSelect, basestring))
def _fill_angular_string(obj, s):
if s:
obj.select_by_visible_text(s)
else:
return
@fill.method((AngularSelect, sel.ByValue))
def _fill_angular_value(obj, s):
if s.value:
obj.select_by_value(s.value)
else:
return
@fill.method((AngularSelect, list))
def _fill_angular_list(obj, l):
for i in l:
fill(obj, i)
@fill.method((AngularCalendarInput, Anything))
def _fill_angular_calendar_input(obj, a):
return obj.fill(a)