class fields(ParametrizedView): # noqa PARAMETERS = ("key", ) input = Input(id=Parameter("key")) select = Select(id=Parameter("key")) param_input = Input(id=ParametrizedString("param_{key}")) dropdown = BootstrapSelect(locator=ParametrizedLocator( ".//div[contains(@class, 'bootstrap-select')]/select[@id={key|quote}]/.." )) param_dropdown = BootstrapSelect(locator=ParametrizedLocator( ".//div[contains(@class, 'bootstrap-select')]/select[@id='param_{key}']/.." )) @property def visible_widget(self): for widget in (self.input, self.dropdown, self.param_input, self.param_dropdown, self.select): try: widget.wait_displayed('2s') return widget except TimedOutError: pass else: raise ItemNotFound("Visible widget is not found") def read(self): return self.visible_widget.read() def fill(self, value): return self.visible_widget.fill(value)
class fields(ParametrizedView): # noqa PARAMETERS = ("key", ) input = Input(id=Parameter("key")) param_input = Input(id=ParametrizedString("param_{key}")) dropdown = VersionPick({ Version.lowest(): BootstrapSelect(Parameter("key")), "5.9": DialogFieldDropDownList( ParametrizedLocator(".//div[@input-id={key|quote}]")) }) param_dropdown = VersionPick({ Version.lowest(): BootstrapSelect(ParametrizedString("param_{key}")), "5.9": DialogFieldDropDownList( ParametrizedLocator(".//div[@input-id='param_{key}']")) }) @property def visible_widget(self): if self.input.is_displayed: return self.input elif self.dropdown.is_displayed: return self.dropdown elif self.param_input.is_displayed: return self.param_input elif self.param_dropdown.is_displayed: return self.param_dropdown def read(self): return self.visible_widget.read() def fill(self, value): return self.visible_widget.fill(value)
class CatalogsMultiBoxSelect(MultiBoxSelect): available_options = Select(locator=ParametrizedLocator( ".//div[contains(text(), {@available_items|quote})]/select")) chosen_options = Select(locator=ParametrizedLocator( ".//div[contains(text(), {@chosen_items|quote})]/select")) move_into_button = Button(title=Parameter("@move_into")) move_from_button = Button(title=Parameter("@move_from"))
class inputs(ParametrizedView): # noqa PARAMETERS = ('name', ) ROOT = ParametrizedLocator('//tr[./td[2]/input[normalize-space(@value)={name|quote}]]') ALL_FIELDS = '//div[@id="inputs_div"]/table//tr/td[2]/input' @cached_property def row_id(self): attr = self.browser.get_attribute( 'id', './/td/input[contains(@id, "fields_name_")]', parent=self) return int(attr.rsplit('_', 1)[-1]) name = Input(locator=ParametrizedLocator( './/td/input[contains(@id, "fields_name_{@row_id}")]')) data_type = Select(locator=ParametrizedLocator( './/td/select[contains(@id, "fields_datatype_{@row_id}")]')) default_value = Input(locator=ParametrizedLocator( './/td/input[contains(@id, "fields_value_{@row_id}")]')) @classmethod def all(cls, browser): results = [] for e in browser.elements(cls.ALL_FIELDS): results.append((browser.get_attribute('value', e), )) return results def delete(self): xpath = './/a/i[contains(@class, "pficon-delete")]' self.browser.click(xpath, parent=self) try: del self.row_id except AttributeError: pass
class ResourceList(Widget): filter = TextInput(locator=ParametrizedLocator( "{@parent_locator}//input[@class='ms-filter']")) ITEM_FROM = ParametrizedLocator( "{@parent_locator}/div[@class='ms-selectable']" "//li[not(contains(@style, 'display: none'))]/span[contains(.,'%s')]" ) ITEM_TO = ParametrizedLocator( "{@parent_locator}/div[@class='ms-selection']" "//li[not(contains(@style, 'display: none'))]/span[contains(.,'%s')]" ) LIST_FROM = ParametrizedLocator( "{@parent_locator}/div[@class='ms-selectable']" "//li[not(contains(@style, 'display: none'))]" ) LIST_TO = ParametrizedLocator( "{@parent_locator}/div[@class='ms-selection']" "//li[not(contains(@style, 'display: none'))]" ) def __init__(self, parent, parent_entity, affected_entity, logger=None): Widget.__init__(self, parent, logger=logger) self.parent_entity = parent_entity.lower() self.affected_entity = affected_entity.lower() self.parent_locator = ( "//div[contains(@id, 'ms-{}') and " "contains(@id, '{}_ids')]".format( self.parent_entity, self.affected_entity) ) def _filter_value(self, value): self.filter.fill(value) def assign_resource(self, values): for value in values: self._filter_value(value) self.browser.click( self.browser.element(self.ITEM_FROM.locator % value)) def unassign_resource(self, values): for value in values: self._filter_value(value) self.browser.click( self.browser.element(self.ITEM_TO.locator % value)) def fill(self, values): if values['operation'] == 'Add': self.assign_resource(values['values']) if values['operation'] == 'Remove': self.unassign_resource(values['values']) def read(self): return { 'free': [ el.text for el in self.browser.elements(self.LIST_FROM)], 'assigned': [ el.text for el in self.browser.elements(self.LIST_TO)] }
class Tab(View): """Represents the Patternfly Tab widget. Selects itself automatically when any child widget gets accessed, ensuring that the widget is visible. https://www.patternfly.org/v4/documentation/react/components/tabs """ # The text on the tab. Can be omitted if it is the same as the tab class name capitalized TAB_NAME = None # Locator of the Tab selector TAB_LOCATOR = ParametrizedLocator( './/div[contains(@class, "pf-c-tabs")]/ul' "/li[button[normalize-space(.)={@tab_name|quote}]]") ROOT = ParametrizedLocator( ".//section[@aria-labelledby=string(" "preceding-sibling::div/ul/li/button[normalize-space(.)={@tab_name|quote}]/@id)]" "|" ".//section[@id=string(../preceding-sibling::div/ul/li" "/button[normalize-space(.)={@tab_name|quote}]/@aria-controls)]") @property def tab_name(self): """Returns the tab name as a string.""" return self.TAB_NAME or type(self).__name__.replace("_", " ").capitalize() def is_active(self): """Returns a boolean detailing of the tab is active.""" return "pf-m-current" in self.parent_browser.classes(self.TAB_LOCATOR) @property def is_displayed(self): """Returns a boolean detailing of the tab is displayed.""" return self.parent_browser.is_displayed(self.TAB_LOCATOR) def click(self): """Clicks the tab.""" return self.parent_browser.click(self.TAB_LOCATOR) def select(self): """Selects the tab (checks if active already first).""" if not self.is_active(): self.logger.info("Opening the tab %s", self.tab_name) @wait_for_decorator(timeout=3) def _click(): self.click() return self.is_active() def child_widget_accessed(self, widget): # Select the tab self.select() def __repr__(self): return "<Tab {!r}>".format(self.tab_name)
class select_packages(ParametrizedView): # noqa title = Text(locator=".//h5[normalize-space(.)='Select packages']") PARAMETERS = ("pkg", ) select_all_packages = Text( locator=".//input[@class='ant-checkbox-input']") packages = Text( ParametrizedLocator( ".//div[contains(@class, 'ant-tree-treenode-switcher-close') " "and not(contains(@class, 'ant-tree-treenode-disabled'))]" "/span/span/span[normalize-space(.)={pkg|quote}]")) included_packages = Text( ParametrizedLocator( ".//li[@class='ant-transfer-list-content-item']" "/span[normalize-space(.)={pkg|quote}]")) move_into_button = Button( locator=".//span[contains(@class, 'anticon anticon-right')]") move_from_button = Button( locator=".//span[contains(@class, 'anticon anticon-left')]") next_button = Button("Next") back_button = Button("Back") cancel_button = Button("Cancel") fill_strategy = WaitFillViewStrategy("15s") @property def is_displayed(self): return self.title.is_displayed and self.select_all_packages.is_displayed def fill_pkg(self): """Add packages""" self.packages.click() self.move_into_button.click() was_change = True return was_change def remove(self): """Remove packages""" self.included_packages.click() self.move_from_button.click() was_change = True return was_change def fill(self, values): """ Args: values: application packages to be selected """ if values.get("pkg"): self.fill_pkg() was_change = True self.after_fill(was_change) return was_change def after_fill(self, was_change): self.next_button.click()
class MappingFillView(ParametrizedView): PARAMETERS = ("object_type", ) source = MultiSelectList(ParametrizedLocator("source_{object_type}")) target = MultiSelectList(ParametrizedLocator("target_{object_type}")) fill_strategy = WaitFillViewStrategy("15s") def after_fill(self, was_change): if not self.parent.add_mapping.disabled: self.parent.add_mapping.click()
class analysis_row(ParametrizedView): # noqa PARAMETERS = ("row", ) analysis_number = Text(ParametrizedLocator(".//tr[{row}]/td[1]/a")) delete_analysis = Text( ParametrizedLocator( ".//tr[{row}]//button[@class='pf-c-button pf-m-link']")) @property def is_displayed(self): return self.analysis_number.is_displayed
class input(ParametrizedView): # noqa PARAMETERS = ("title", ) field_enable = Text(ParametrizedLocator( ".//*[(self::input or self::textarea) and " "@title={title|quote}]/../../a[text()='Update']")) field_disable = Text(ParametrizedLocator( ".//*[(self::input or self::textarea) and " "@title={title|quote}]/../../a[text()='Cancel']")) def toggle(self): if self.field_enable.is_displayed: self.field_enable.click() elif self.field_disable.is_displayed: self.field_disable.click()
class MappingFillView(ParametrizedView): PARAMETERS = ("object_type",) source = MultiSelectList(ParametrizedLocator("source_{object_type}")) target = MultiSelectList(ParametrizedLocator("target_{object_type}")) fill_strategy = WaitFillViewStrategy("15s") def after_fill(self, was_change): if not self.parent.add_mapping.disabled: self.parent.add_mapping.click() @property def is_displayed(self): return (self.source.is_displayed and (len(self.browser.elements(".//div[contains(@class,'spinner')]")) == 0))
class BootstrapSelectByLocator(BootstrapSelect): """Modified :py:class:`widgetastic_patternfly.BootstrapSelect` that uses the div locator.""" ROOT = ParametrizedLocator('{@locator}') def __init__(self, parent, locator, can_hide_on_select=False, logger=None): BootstrapSelect.__init__(self, parent, locator, can_hide_on_select, logger) self.locator = locator
class fields(ParametrizedView): # noqa PARAMETERS = ('name', ) ROOT = ParametrizedLocator( './/tr[./td[contains(normalize-space(.), {name|quote})]]') @cached_property def row_id(self): attr = self.browser.get_attribute( 'id', './td/select[starts-with(@id, "per_time_")]', parent=self) return int(attr.rsplit('_', 1)[-1]) @cached_property def sub_row_id(self): attr = self.browser.get_attribute( 'id', './td/input[starts-with(@id, "fixed_rate_")]', parent=self) return int(attr.rsplit('_', 1)[-1]) per_time = Select(id=ParametrizedString('per_time_{@row_id}')) per_unit = Select(id=ParametrizedString('per_unit_{@row_id}')) start = Input(id=ParametrizedString('start_{@row_id}_{@sub_row_id}')) finish = Input(id=ParametrizedString('finish_{@row_id}_{@sub_row_id}')) fixed_rate = Input( id=ParametrizedString('fixed_rate_{@row_id}_{@sub_row_id}')) variable_rate = Input( id=ParametrizedString('variable_rate_{@row_id}_{@sub_row_id}')) action_add = Button(title='Add a new tier') action_delete = Button(title='Remove the tier')
class fields(ParametrizedView): # noqa PARAMETERS = ('name', ) ROOT = ParametrizedLocator( './/tr[./td[1][contains(normalize-space(.), "({name})")]]') ALL_FIELDS = './/table//tr/td[1]' @cached_property def row_id(self): attr = self.browser.get_attribute( 'id', './td/input[starts-with(@id, "cls_inst_value_")]', parent=self) return int(attr.rsplit('_', 1)[-1]) value = Input(name=ParametrizedString('cls_inst_value_{@row_id}')) on_entry = Input( name=ParametrizedString('cls_inst_on_entry_{@row_id}')) on_exit = Input(name=ParametrizedString('cls_inst_on_exit_{@row_id}')) on_error = Input( name=ParametrizedString('cls_inst_on_error_{@row_id}')) collect = Input(name=ParametrizedString('cls_inst_collect_{@row_id}')) @classmethod def all(cls, browser): results = [] for e in browser.elements(cls.ALL_FIELDS): text = re.sub(r'^\(|\)$', '', browser.text(e)) results.append((text, )) return results
class BootstrapSwitch(Checkbox): """ represents checkbox like switch control. widgetastic checkbox doesn't work right for this control. So, this widget is some kind of enhancement .. code-block:: python switch = BootstrapSwitch(id="default_tls_verify"') switch.fill(True) switch.read() """ ROOT = ParametrizedLocator( '//div[contains(@class, "bootstrap-switch-container") and ' '{@input}]') def __init__(self, parent, id=None, name=None, logger=None): if not (id or name): raise ValueError('either id or name should be present') elif name is not None: id_attr = '@name={}'.format(quote(name)) else: id_attr = '@id={}'.format(quote(id)) self.input = './/input[{}]'.format(id_attr) Checkbox.__init__(self, parent, locator=self.ROOT, logger=logger) @property def selected(self): return self.browser.is_selected(parent=self, locator=self.input)
class OUIABase: """ Base class for ``OUIA`` support. According to the spec ``OUIA`` compatible components may have the following attributes in the root level HTML element: * data-ouia-component-type * data-ouia-component-id * data-ouia-safe https://ouia.readthedocs.io/en/latest/README.html#ouia-component """ ROOT = ParametrizedLocator( ".//*[@data-ouia-component-type={@component_type}{@component_id}]") def __init__(self, component_type, component_id=None, namespace=None, **kwargs): component_type = f"{namespace}/{component_type}" if namespace else component_type self.component_type = quote(component_type) component_id = f" and @data-ouia-component-id={quote(component_id)}" if component_id else "" self.component_id = component_id self.locator = self.ROOT.locator super().__init__(**kwargs) @property def is_safe(self): """ An attribute called data-ouia-safe, which is True only when the component is in a static state, i.e. no animations are occurring. At all other times, this value MUST be False. """ return "true" in self.browser.get_attribute("data-ouia-safe", self) def __locator__(self): return self.ROOT
class Accordion(View, ClickableMixin): """Bootstrap accordions. They are like views that contain widgets. If a widget is accessed in the accordion, the accordion makes sure that it is open. You need to set the ``ACCORDION_NAME`` to correspond with the text in the accordion. If the accordion title is just a capitalized version of the accordion class name, you do not need to set the ``ACCORDION_NAME``. If the accordion is in an exotic location, you also have to change the ``ROOT``. https://getbootstrap.com/docs/4.1/components/collapse/#accordion-example """ ACCORDION_NAME = None ROOT = ParametrizedLocator( './/div[contains(@class, "accordion")]//div[@class="card" and ' './/div[@class="card-header"]//button[normalize-space(.)={@accordion_name|quote}]]' ) HEADER_LOCATOR = ".//button" @property def accordion_name(self): return self.ACCORDION_NAME or type(self).__name__.capitalize() @property def is_opened(self): attr = self.browser.get_attribute("aria-expanded", self.HEADER_LOCATOR) return attr.lower().strip() == "true" @property def is_closed(self): return not self.is_opened def click(self): """Override Clickable's click.""" self.browser.click(self.HEADER_LOCATOR) def open(self): if self.is_closed: self.logger.info("opening") self.click() wait_for(lambda: self.is_opened, delay=0.1, num_sec=3) def close(self): if self.is_opened: self.logger.info("closing") self.click() def child_widget_accessed(self, widget): # Open the Accordion self.open() def read(self): if self.is_closed: do_not_read_this_widget() return super(Accordion, self).read() def __repr__(self): return '<Accordion {!r}>'.format(self.accordion_name)
class legends(ParametrizedView): # noqa PARAMETERS = ('name', ) ALL_LEGENDS = './/kubernetes-topology-icon//label' el = Text( ParametrizedLocator( './/kubernetes-topology-icon//label[normalize-space(.)={name|quote}]' )) @property def is_enabled(self): el = self.browser.element( './ancestor::kubernetes-topology-icon[1]', self.el) return 'active' in self.browser.get_attribute('class', el) def enable(self): if not self.is_enabled: self.el.click() def disable(self): if self.is_enabled: self.el.click() @property def name(self): return self.el.text @classmethod def all(cls, browser): return [(browser.text(e), ) for e in browser.elements(cls.ALL_LEGENDS)]
class Tab(View, ClickableMixin): """Represents the Tab widget. Selects itself automatically when any child widget gets accessed, ensuring that the widget is visible. """ TAB_NAME = None INDIRECT = True ROOT = ParametrizedLocator( './/ul[contains(@class, "nav-tabs")]/li[normalize-space(.)={@tab_name|quote}]') @property def tab_name(self): return self.TAB_NAME or type(self).__name__.capitalize() def is_active(self): return 'active' in self.browser.classes(self) def is_disabled(self): return 'disabled' in self.browser.classes(self) def select(self): if not self.is_active(): if self.is_disabled(): raise ValueError( 'The tab {} you are trying to select is disabled'.format(self.tab_name)) self.logger.info('opened the tab %s', self.tab_name) self.click() def child_widget_accessed(self, widget): # Select the tab self.select() def __repr__(self): return '<Tab {!r}>'.format(self.tab_name)
class TabsView(View): ROOT = ParametrizedLocator(".//ul[@id={@id|quote}]/..") tabs_names = ["home", "profile", "disabled", "dropdown"] def __init__(self, parent, id, logger=None): super(TabsView, self).__init__(parent, logger=logger) self.id = id @View.nested class home(Tab): pass @View.nested class profile(Tab): PROFILE = ParametrizedLocator("//div[@id=concat({@parent/id|quote}, '-profile')]") content = Text(PROFILE) @View.nested class disabled(Tab): pass @View.nested class dropdown(TabWithDropdown): SUB_ITEM = "Action" pass
class Alert(Widget): """Represents alert block. https://www.patternfly.org/v4/documentation/react/components/alert """ ROOT = ParametrizedLocator("{@locator}") TITLE = './/h4[@class="pf-c-alert__title"]' DESCRIPTION = './/div[@class="pf-c-alert__description"]' ACTION = './/div[@class="pf-c-alert__action"]/*' TYPE_MAPPING = { "pf-m-warning": "warning", "pf-m-success": "success", "pf-m-danger": "error", "pf-m-info": "info", } def __init__(self, parent, locator, logger=None): super(Alert, self).__init__(parent, logger=logger) self.locator = locator def read(self): return self.body @property def _raw_title_el(self): return self.browser.element(self.TITLE) @property def title(self): trim_text = self.browser.text( self.browser.element("./span", parent=self._raw_title_el)) return self.browser.text(self._raw_title_el)[len(trim_text):].strip() @property def body(self): el = self.browser.element(self.DESCRIPTION) return self.browser.text(el) def click_action(self): el = self.browser.element(self.ACTION) self.browser.click(el) @property def type(self): for class_ in self.browser.classes(self): if class_ in self.TYPE_MAPPING: return self.TYPE_MAPPING[class_] else: raise ValueError("Could not find a proper alert type." " Available classes: {!r} Alert has: {!r}".format( self.TYPE_MAPPING, self.browser.classes(self))) def assert_no_error(self): if self.type == "error": raise AssertionError("assert_no_error: {}".format(self.body)) def __repr__(self): return "{}({!r})".format(type(self).__name__, self.locator)
class fields(ParametrizedView): # noqa PARAMETERS = ("key", ) input = Input(id=Parameter("key")) select = Select(id=Parameter("key")) param_input = Input(id=ParametrizedString("param_{key}")) dropdown = VersionPick({ Version.lowest(): BootstrapSelect(Parameter("key")), "5.9": BootstrapSelect(locator=ParametrizedLocator( './/div[contains(@class, "bootstrap-select")]/select[@id={key|quote}]/..' )) }) param_dropdown = VersionPick({ Version.lowest(): BootstrapSelect(ParametrizedString("param_{key}")), "5.9": BootstrapSelect(locator=ParametrizedLocator( ".//div[contains(@class, 'bootstrap-select')]/select[@id='param_{key}']/.." )) }) @property def visible_widget(self): if self.browser.wait_for_element(self.input.locator, exception=False): return self.input elif self.browser.wait_for_element(self.dropdown.locator, exception=False): return self.dropdown elif self.browser.wait_for_element(self.param_input.locator, exception=False): return self.param_input elif self.browser.wait_for_element(self.param_dropdown.locator, exception=False): return self.param_dropdown elif self.browser.wait_for_element(self.select.locator, exception=False): return self.select else: raise ItemNotFound("Visible widget is not found") def read(self): return self.visible_widget.read() def fill(self, value): return self.visible_widget.fill(value)
class fields(ParametrizedView): # noqa PARAMETERS = ('name', ) # Points to the <tr> ROOT = ParametrizedLocator( './/input[starts-with(@id, "fields_name_") and @value={name|quote}]/../..' ) ALL_FIELDS = './/input[starts-with(@name, "fields_name_")]' @cached_property def row_id(self): attr = self.browser.get_attribute( 'id', './td/input[starts-with(@id, "fields_name_")', parent=self) return int(attr.rsplit('_', 1)[-1]) name = Input(name=ParametrizedString('fields_name_{@row_id}')) type = BootstrapSelect( ParametrizedString('fields_aetype_{@row_id}')) data_type = BootstrapSelect( ParametrizedString('fields_datatype_{@row_id}')) default_value = Input( name=ParametrizedString('fields_default_value_{@row_id}')) display_name = Input( name=ParametrizedString('fields_display_name_{@row_id}')) description = Input( name=ParametrizedString('fields_description_{@row_id}')) substitute = Checkbox( name=ParametrizedString('fields_substitute_{@row_id}')) collect = Input( name=ParametrizedString('fields_collect_{@row_id}')) message = Input( name=ParametrizedString('fields_message_{@row_id}')) on_entry = Input( name=ParametrizedString('fields_on_entry_{@row_id}')) on_exit = Input( name=ParametrizedString('fields_on_exit_{@row_id}')) on_error = Input( name=ParametrizedString('fields_on_error_{@row_id}')) max_retries = Input( name=ParametrizedString('fields_max_retries_{@row_id}')) max_time = Input( name=ParametrizedString('fields_max_time_{@row_id}')) def delete(self): self.browser.click( './/img[@alt="Click to delete this field from schema"]', parent=self) try: del self.row_id except AttributeError: pass @classmethod def all(cls, browser): result = [] for e in browser.elements(cls.ALL_FIELDS): result.append((browser.get_attribute('value', e), )) return result
class Button(BaseButton, Widget, ClickableMixin): """A Patternfly button You can match by text, partial text or by attributes, you can also add the patternfly classes into the matching. .. code-block:: python Button("Text of button (unless it is an input ...)") Button("contains", "Text of button (unless it is an input ...)") Button(title="Show xyz") # And such Button("Add", classes=[Button.PRIMARY]) Button(locator=".//xpath") assert button.active assert not button.disabled """ ROOT = ParametrizedLocator("{@locator}") def _generate_locator(self, *text, **kwargs): classes = kwargs.pop("classes", []) if text: if kwargs: # classes should have been the only kwarg combined with text args raise TypeError( "If you pass button text then only pass classes in addition" ) if len(text) == 1: locator_conditions = "normalize-space(.)={}".format( quote(text[0])) elif len(text) == 2 and text[0].lower() == "contains": locator_conditions = "contains(normalize-space(.), {})".format( quote(text[1])) else: raise TypeError("An illegal combination of args/kwargs") else: # Join the kwargs, if any locator_conditions = " and ".join( "@{}={}".format(attr, quote(value)) for attr, value in kwargs.items()) if classes: if locator_conditions: locator_conditions += " and " locator_conditions += " and ".join( "contains(@class, {})".format(quote(klass)) for klass in classes) if locator_conditions: locator_conditions = "and ({})".format(locator_conditions) return (".//*[(self::a or self::button or (self::input and " "(@type='button' or @type='submit'))) and " f"contains(@class, 'pf-c-button') {locator_conditions}]") def __init__(self, parent, *text, **kwargs): super().__init__(parent, logger=kwargs.pop("logger", None)) self.args = text self.kwargs = kwargs self.locator = kwargs.pop("locator", self._generate_locator(*text, **kwargs))
class Kebab(Dropdown): """The so-called "kebab" widget of Patternfly.""" ROOT = ParametrizedLocator("{@locator}") def __init__(self, parent, locator, logger=None): Widget.__init__(self, parent, logger=logger) self.locator = locator
class Pagination(BasePagination, View): ROOT = ParametrizedLocator("{@locator}") def __init__(self, parent, locator=None, logger=None): super().__init__(parent=parent, logger=logger) if not locator: locator = self.DEFAULT_LOCATOR self.locator = locator
class PlaybookBootstrapSelect(BootstrapSelect): """BootstrapSelect widget for Ansible Playbook Method form. BootstrapSelect widgets don't have ``data-id`` attribute in this form, so we have to override ROOT locator. """ ROOT = ParametrizedLocator('.//select[normalize-space(@name)={@id|quote}]/..')
class Tab(View): """Represents the Tab widget. Selects itself automatically when any child widget gets accessed, ensuring that the widget is visible. https://getbootstrap.com/docs/4.1/components/navs/#tabs You can specify your own ``ROOT`` attribute on the class. """ ROOT = None #: The text on the tab. If it is the same as the tab class name capitalized, can be omitted TAB_NAME = None # Locator of the Tab selector TAB_ROOT = ParametrizedLocator( './/ul[contains(@class, "nav-tabs") or contains(@class, "nav-pills")]' '/li[./a[normalize-space(.)={@tab_name|quote}]]') NAV_LINK = "./a[contains(@class, 'nav-link')]" def __locator__(self): return self.ROOT or self.TAB_ROOT @property def tab_name(self): return self.TAB_NAME or type(self).__name__.capitalize() @property def _tab_el(self): return self.parent_browser.element(self.TAB_ROOT) @property def is_active(self): return "active" in self.browser.classes(self.NAV_LINK, parent=self._tab_el) @property def is_disabled(self): return "disabled" in self.browser.classes(self.NAV_LINK, parent=self._tab_el) def click(self): return self.browser.click(self._tab_el) def select(self): if not self.is_active: if self.is_disabled: raise ValueError( "The tab {} you are trying to select is disabled".format( self.tab_name)) self.logger.info("opened the tab %s", self.tab_name) self.click() def child_widget_accessed(self, widget): # Select the tab self.select() def __repr__(self): return "<Tab {!r}>".format(self.tab_name)
class Card(View): ROOT = ParametrizedLocator( '//div[contains(@class, "card") and boolean(@data-card) and @data-card={@id|quote}]' ) ALL_CARDS = '//div[contains(@class, "card") and boolean(@data-card)]' TITLE = './/h6' DESCRIPTION = './/div[contains(@class, "card-body")]/p[1]' like_button = GenericLocatorWidget( locator='.//button[./*[name()="svg" and @data-icon="thumbs-up"]]') delete_button = Button('Delete') def __init__(self, parent, id, logger=None): View.__init__(self, parent, logger=logger) self.id = id @property def title(self): return self.browser.text(self.TITLE) @property def description(self): return self.browser.text(self.DESCRIPTION) @property def liked(self): return bool(int(self.browser.text(self.like_button))) def wait_liked(self, liked, timeout='5s'): return wait_for( lambda: self.like_button.is_displayed and self.liked == liked, timeout=timeout, delay=1)[0] def like(self): if not self.liked: self.like_button.click() return True else: return False def unlike(self): if self.liked: self.like_button.click() return True else: return False def delete(self): self.delete_button.click() @classmethod def all(cls, parent): cards = [] for el in parent.browser.elements(cls.ALL_CARDS): card_id = parent.browser.get_attribute('data-card', el) cards.append(cls(parent, id=card_id)) return cards
class _card(ParametrizedView): PARAMETERS = ("card_name", ) card = Text( ParametrizedLocator( "//h4[contains(normalize-space(.), {card_name|quote})]")) def click_card(self): """Clicks the list item with this name.""" return self.card.click()