class LoginPage(View): navbar = View.nested(Navbar) username = TextInput(id="username") password = TextInput(id="password") remember_me = Checkbox(id="remember_me") login_button = GenericLocatorWidget('.//input[@name="submit"]') register = GenericLocatorWidget(".//a[@href='/auth/register']") @property def is_displayed(self): return False
def edit_credential(view, original_name, options): """Edit a credential through the UI and verify it was edited. :param view: The view context (should be the browser view) :param original_name: The original name of the credential. :param options: The options to be edited within the credential. """ view.refresh() dash = DashboardView(view) dash.nav.select("Credentials") view.wait_for_element(locator=Locator(xpath=(edit_xpath(original_name)))) GenericLocatorWidget( view, locator=Locator(xpath=edit_xpath(original_name))).click() modal = CredentialModalView(view, locator=Locator(css=".modal-content")) wait_for_animation(1) fill_credential_info(view, options) # Hack to deal with the fact that the GET refresh isn't # implemented when the save button is clicked. # https://github.com/quipucords/quipucords/issues/1399 # https://github.com/quipucords/camayoc/issues/280 wait_for_animation() modal.save_button.click() wait_for_animation() view.refresh() dash.nav.select("Credentials") # Assert the row with the credential name exists. # If the name was updated, use the new name. current_name = original_name if "name" in options: current_name = options["name"] view.wait_for_element(locator=Locator(xpath=row_xpath(current_name)), delay=0.5, timeout=10) GenericLocatorWidget( view, locator=Locator(xpath=edit_xpath(current_name))).click() modal = CredentialModalView(view, locator=Locator(css=".modal-content")) # Assert that the changed variables were in fact changed. # Passwords are skipped because they aren't accessible. for option, data in options.items(): if ((option == "password") or (option == "become_pass") or (option == "source_type")): continue browser_data = get_field_value(view, CREDENTIAL_FIELD_LABELS[option]) if option == "sshkeyfile": # tmp files are resolved with alias prefixes in some cases. # the characters afer the final '/' remain consistent. assert browser_data.rpartition("/")[2] == data.rpartition("/")[2] else: assert browser_data == data
class ProfileDetialsView(View): title = Text(".//h1") edit = GenericLocatorWidget(".//a[normalize-space(.)='Edit your profile']") @property def is_displayed(self): return self.title.text == "User: {}".format( self.context["object"].username)
class ProfileEditView(View): title = Text(".//h1") username = TextInput(name="username") about = TextInput(name="about_me") submit = GenericLocatorWidget(".//input[@name='submit']") @property def is_displayed(self): return self.title.text == "Edit Profile"
class AllPostsView(BaseLoggedInView): greeting = Text(".//h1") text_area = TextInput(name="post") submit = GenericLocatorWidget('.//input[@name="submit"]') posts = ParametrizedView.nested(PostView) @property def is_displayed(self): return (self.logged_in and self.greeting.text == "Hi, {}!".format( self.context["object"].username))
class OperatingSystemView(BaseLoggedInView, SearchableViewMixin): title = Text("//h1[text()='Operating systems']") new = Text("//a[contains(@href, '/operatingsystems/new')]") delete = GenericLocatorWidget( "//span[contains(@class, 'btn')]/a[@data-method='delete']") @property def is_displayed(self): return self.browser.wait_for_element(self.title, exception=False) is not None
class BaseLoggedInView(View): navbar = View.nested(Navbar) greeting = Text(".//h1") text_area = TextInput(name="post") submit = GenericLocatorWidget('.//input[@name="submit"]') newer_posts = Text(".//a[contains(normalize-space(.), 'Newer posts')]") older_posts = Text(".//a[contains(normalize-space(.), 'Older posts')]") @property def is_displayed(self): return self.newer_posts.is_displayed and self.older_posts.is_displayed
def delete_source(view, source_name): """Delete a source through the UI.""" clear_toasts(view=view) dash = DashboardView(view) dash.nav.select("Sources") wait_for_animation() view.wait_for_element(locator=Locator(xpath=(delete_xpath(source_name)))) GenericLocatorWidget( view, locator=Locator(xpath=delete_xpath(source_name))).click() # mitigate database lock issue quipucords/quipucords/issues/1275 wait_for_animation() DeleteModalView(view).delete_button.click() wait_for_animation() clear_toasts(view=view) with pytest.raises(NoSuchElementException): view.wait_for_element(locator=Locator(xpath=delete_xpath(source_name)), timeout=2)
def create_source(view, credential_name, source_type, source_name, addresses): """Create a source through the UI.""" clear_toasts(view=view) dash = DashboardView(view) dash.nav.select("Sources") # Display varies depending on whether or not sources already exist. wait_for_animation(1) try: Button(view, "Add Source").click() except NoSuchElementException: Button(view, "Add").click() # Source creation wizard modal = SourceModalView(view, locator=Locator(css=".modal-content")) radio_label = SOURCE_TYPE_RADIO_LABELS[source_type] # Wait for radio button to become responsive before clicking a source type. wait_for_animation() GenericLocatorWidget( modal, locator=Locator(xpath=radio_xpath(radio_label))).click() wait_for_animation(1) modal.next_button.click() # Fill in required source information. fill(modal, field_xpath("Name"), source_name) if source_type == "Network": fill(modal, field_xpath("Search Addresses", textarea=True), addresses) fill(modal, field_xpath("Port"), "") # default port of 22 cred_dropdown = Dropdown(modal, "Select one or more credentials") cred_dropdown.item_select(credential_name) else: fill(modal, field_xpath("IP Address or Hostname"), addresses) cred_dropdown = Dropdown(modal, "Select a credential") cred_dropdown.item_select(credential_name) Button(modal, "Save").click() wait_for_animation(2) view.wait_for_element(locator=Locator('//button[text()="Close"]')) Button(modal, "Close", classes=[Button.PRIMARY]).click() wait_for_animation(1) # mitigate database lock issue quipucords/quipucords/issues/1275 clear_toasts(view=view) # Verify that the new row source has been created. view.wait_for_element(locator=Locator(xpath=row_xpath(source_name))) view.element(locator=Locator(xpath=row_xpath(source_name)))
def clear_toasts(view, count=20): """Attempt to flush any confirmation dialogs that may have appeared. Use this function to clear out dialogs (toasts) that may be preventing buttons from being clicked properly. Sometimes it might need to be used in succession. By default, this tries to flush a maximum of 20 toasts, but will quit early if it cannot find more. """ for i in range(count): try: view.wait_for_element(locator=Locator(css=".close"), timeout=0.6) GenericLocatorWidget(view, locator=Locator(css=".close")).click() except ( MoveTargetOutOfBoundsException, NoSuchElementException, StaleElementReferenceException, ): break
class PostView(ParametrizedView): PARAMETERS = ("post_id", ) ALL_POSTS = ".//span[contains(@id, 'post')]" delete_link = GenericLocatorWidget( ParametrizedLocator(".//a[@href='/delete/{post_id}']")) @classmethod def all(cls, browser): return [(int(e.get_attribute("id").lstrip("post")), ) for e in browser.elements(cls.ALL_POSTS)] @classmethod def _last_post_id(cls, browser): try: return max(cls.all(browser)) except ValueError: return 0, def delete(self): self.delete_link.click() @property def is_displayed(self): return self.delete_link.is_displayed
class Pagination(View): """Represents the Patternfly pagination. https://www.patternfly.org/v4/documentation/react/components/pagination """ ROOT = ParametrizedLocator("{@locator}") DEFAULT_LOCATOR = ( ".//div[contains(@class, 'pf-c-pagination') and not(contains(@class, 'pf-m-compact'))]" ) _first = GenericLocatorWidget(".//button[contains(@data-action, 'first')]") _previous = GenericLocatorWidget( ".//button[contains(@data-action, 'previous')]") _next = GenericLocatorWidget(".//button[contains(@data-action, 'next')]") _last = GenericLocatorWidget(".//button[contains(@data-action, 'last')]") _options = OptionsMenu() _items = Text(".//span[@class='pf-c-options-menu__toggle-text']") _current_page = TextInput(locator=".//input[@aria-label='Current page']") _total_pages = Text( ".//div[@class='pf-c-pagination__nav-page-select']/span") def __init__(self, parent, locator=None, logger=None): View.__init__(self, parent=parent, logger=logger) if not locator: locator = self.DEFAULT_LOCATOR self.locator = locator self._cached_per_page_value = None @property def is_first_disabled(self): """Returns boolean detailing if the first page button is disabled.""" return not self.browser.element(self._first).is_enabled() def first_page(self): """Clicks on the first page button.""" if self.no_items or self.is_first_disabled: raise PaginationNavDisabled("first") self._first.click() @property def is_previous_disabled(self): """Returns boolean detailing if the previous page button is disabled.""" return not self.browser.element(self._previous).is_enabled() def previous_page(self): """Clicks the previous page button.""" if self.no_items or self.is_previous_disabled: raise PaginationNavDisabled("previous") self._previous.click() @property def is_next_disabled(self): """Returns boolean detailing if the next page button is disabled.""" return not self.browser.element(self._next).is_enabled() def next_page(self): """Clicks the next page button.""" if self.is_next_disabled: raise PaginationNavDisabled("next") self._next.click() @property def is_last_disabled(self): """Returns boolean detailing if the last page button is disabled.""" return not self.browser.element(self._last).is_enabled() def last_page(self): """Clicks the last page button.""" if self.is_last_disabled: raise PaginationNavDisabled("last") self._last.click() @property def current_page(self): """Returns an int of the current page number.""" return int(self._current_page.value) @property def total_pages(self): """Returns int detailing the total number of pages.""" return int(self._total_pages.text.strip().split()[1]) @property def displayed_items(self): """Returns a string detailing the number of displayed items information. example "1 - 20 of 523 items" """ items_string = self._items.text first_num, last_num = items_string.split("of")[0].split("-") return int(first_num.strip()), int(last_num.strip()) @property def total_items(self): """Returns a string detailing the number of displayed items""" items_string = self._items.text return int(items_string.split("of")[1].split()[0]) @property def per_page_options(self): """Returns an iterable of the available pagination options.""" return self._options.items @property def no_items(self): """Returns wether the pagination object has elements or not""" return not self.total_items @property def current_per_page(self): """Returns an integer detailing how many items are cshown per page.""" if self._cached_per_page_value: return self._cached_per_page_value if self.no_items: return 0 else: return int(self._options.selected_items[0].split()[0]) @contextmanager def cache_per_page_value(self): """ A context manager that can be used to prevent looking up the 'current page' value. This adds some efficiencies when iterating over pages or in cases where it is safe to assume that the "per page" setting is not going to change and it's not necessary to re-read it from the browser repeatedly. """ self._cached_per_page_value = None self._cached_per_page_value = self.current_per_page yield self._cached_per_page_value = None def set_per_page(self, count): """Sets the number of items per page. (Will cast to str)""" value = str(count) value_per_page = "{} per page".format(value) items = self._options.items if value_per_page in items: self._options.item_select(value_per_page) elif value in items: self._options.item_select(value) else: raise ValueError( "count '{}' is not a valid option in the pagination dropdown". format(count)) def go_to_page(self, value): """Navigate to custom page number.""" self._current_page.fill(value) self.browser.send_keys(Keys.RETURN, self._current_page) def __iter__(self): if self.current_page > 1: self.first_page() self._page_counter = 0 return self def __next__(self): if self._page_counter < self.total_pages: self._page_counter += 1 if self._page_counter > 1: self.next_page() return self._page_counter else: raise StopIteration
class Pagination(View): """Represents the Patternfly pagination. https://www.patternfly.org/v4/documentation/react/components/pagination """ ROOT = ParametrizedLocator("{@locator}") _first = GenericLocatorWidget(".//button[contains(@data-action, 'first')]") _previous = GenericLocatorWidget(".//button[contains(@data-action, 'previous')]") _next = GenericLocatorWidget(".//button[contains(@data-action, 'next')]") _last = GenericLocatorWidget(".//button[contains(@data-action, 'last')]") _options = Dropdown() _items = Text(".//span[@class='pf-c-options-menu__toggle-text']") _current_page = TextInput(locator=".//input[@aria-label='Current page']") _total_pages = Text(".//div[@class='pf-c-pagination__nav-page-select']/span") def __init__(self, parent, locator, logger=None): View.__init__(self, parent=parent, logger=logger) self.locator = locator @property def is_first_disabled(self): return "pf-m-disabled" in self.browser.classes(self._first) def first_page(self): self._first.click() @property def is_previous_disabled(self): return "pf-m-disabled" in self.browser.classes(self._previous) def previous_page(self): self._previous.click() @property def is_next_disabled(self): return "pf-m-disabled" in self.browser.classes(self._next) def next_page(self): self._next.click() @property def is_last_disabled(self): return "pf-m-disabled" in self.browser.classes(self._last) def last_page(self): self._last.click() @property def current_page(self): return int(self._current_page.value) @property def total_pages(self): # example "of 6 pages" return int(self._total_pages.text.strip().split()[1]) @property def displayed_items(self): items_string = self._items.text # example "1 - 20 of 523 items" first_num, last_num = items_string.split("of")[0].split("-") return int(first_num.strip()), int(last_num.strip()) @property def total_items(self): items_string = self._items.text return int(items_string.split("of")[1].split()[0]) @property def per_page_options(self): return self._options.items def set_per_page(self, count): # convert a possible int to string value = str(count) value_per_page = "{} per page".format(value) items = self._options.items if value_per_page in items: self._options.item_select(value_per_page) elif value in items: self._options.item_select(value) else: raise ValueError( "count '{}' is not a valid option in the pagination dropdown".format(count) ) def __iter__(self): self.first_page() self._page_counter = 0 return self def __next__(self): if self._page_counter < self.total_pages: self._page_counter += 1 if self._page_counter > 1: self.next_page() return self._page_counter else: raise StopIteration def next(self): # For sake Python 2 compatibility return self.__next__()