diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index c08fe96f2..fa2275d92 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -4,8 +4,10 @@ from airgun.navigation import NavigateStep, navigator from airgun.utils import retry_navigation from airgun.views.contentview_new import ( - NewContentViewCreateView, - NewContentViewTableView, + ContentViewCreateView, + ContentViewEditView, + ContentViewTableView, + ContentViewVersionPublishView, ) @@ -15,20 +17,40 @@ class NewContentViewEntity(BaseEntity): def create(self, values): """Create a new content view""" view = self.navigate_to(self, 'New') + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() view.fill(values) view.submit.click() def search(self, value): """Search for content view""" view = self.navigate_to(self, 'All') + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() return view.search(value) + def publish(self, entity_name, values=None): + """Publishes new version of CV""" + view = self.navigate_to(self, 'Publish', entity_name=entity_name) + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() + if values: + view.fill(values) + view.next.click() + view.finish.click() + view.progressbar.wait_for_result(delay=0.01) + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() + view.versions.table.wait_displayed() + return view.versions.table.read() + @navigator.register(NewContentViewEntity, 'All') class ShowAllContentViewsScreen(NavigateStep): """Navigate to All Content Views screen.""" - VIEW = NewContentViewTableView + VIEW = ContentViewTableView @retry_navigation def step(self, *args, **kwargs): @@ -39,9 +61,40 @@ def step(self, *args, **kwargs): class CreateContentView(NavigateStep): """Navigate to Create content view.""" - VIEW = NewContentViewCreateView + VIEW = ContentViewCreateView prerequisite = NavigateToSibling('All') def step(self, *args, **kwargs): self.parent.create_content_view.click() + + +@navigator.register(NewContentViewEntity, 'Edit') +class EditContentView(NavigateStep): + """Navigate to Edit Content View screen.""" + + VIEW = ContentViewEditView + + def prerequisite(self, *args, **kwargs): + return self.navigate_to(self.obj, 'All') + + def step(self, *args, **kwargs): + entity_name = kwargs.get('entity_name') + self.parent.search(entity_name) + self.parent.table.row(name=entity_name)['Name'].widget.click() + + +@navigator.register(NewContentViewEntity, 'Publish') +class PublishContentViewVersion(NavigateStep): + """Navigate to Content View Publish screen.""" + + VIEW = ContentViewVersionPublishView + + def prerequisite(self, *args, **kwargs): + """Open Content View first.""" + return self.navigate_to(self.obj, 'Edit', entity_name=kwargs.get('entity_name')) + + @retry_navigation + def step(self, *args, **kwargs): + """Click 'Publish new version' button""" + self.parent.publish.click() diff --git a/airgun/views/common.py b/airgun/views/common.py index 55f0a4432..ffe15a9cc 100644 --- a/airgun/views/common.py +++ b/airgun/views/common.py @@ -1,3 +1,6 @@ +import time + +import wait_for from widgetastic.widget import ( Checkbox, ConditionalSwitchableView, @@ -11,7 +14,7 @@ ) from widgetastic_patternfly import BreadCrumb, Button, Tab, TabWithDropdown from widgetastic_patternfly4.navigation import Navigation -from widgetastic_patternfly4.ouia import Dropdown +from widgetastic_patternfly4.ouia import Button as PF4Button, Dropdown, PatternflyTable from airgun.utils import get_widget_by_name, normalize_dict_values from airgun.widgets import ( @@ -383,6 +386,73 @@ class add_tab(AddTab): ) +class NewAddRemoveResourcesView(View): + searchbox = PF4Search() + type = Dropdown( + locator='.//div[contains(@class, "All repositories") or' + ' contains(@aria-haspopup="listbox")]' + ) + Status = Dropdown( + locator='.//div[contains(@class, "All") or contains(@aria-haspopup="listbox")]' + ) + add_repo = PF4Button('OUIA-Generated-Button-secondary-2') + # Need to add kebab menu + table = PatternflyTable( + component_id='OUIA-Generated-Table-4', + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'Type': Text('.//a'), + 'Name': Text('.//a'), + 'Product': Text('.//a'), + 'Sync State': Text('.//a'), + 'Content': Text('.//a'), + 'Status': Text('.//a'), + }, + ) + + def search(self, value): + """Search for specific available resource and return the results""" + self.searchbox.search(value) + # Tried following ways to wait for table to be displayed, only sleep worked + # Might need a before/after fill + wait_for( + lambda: self.table.is_displayed is True, + timeout=60, + delay=1, + ) + time.sleep(3) + self.table.wait_displayed() + return self.table.read() + + def add(self, value): + """Associate specific resource""" + self.search(value) + next(self.table.rows())[0].widget.fill(True) + self.add_repo.click() + + def fill(self, values): + """Associate resource(s)""" + if not isinstance(values, list): + values = [ + values, + ] + for value in values: + self.add(value) + + def remove(self, value): + """Unassign some resource(s). + :param str or list values: string containing resource name or a list of + such strings. + """ + self.search(value) + next(self.table.rows())[0].widget.fill(True) + self.remove_button.click() + + def read(self): + """Read all table values from both resource tables""" + return self.table.read() + + class TemplateEditor(View): """Default view for template entity editor that can be present for example on provisioning template of partition table pages. It contains from diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py index 5b558c04f..5def17d15 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -1,10 +1,94 @@ +from wait_for import wait_for +from widgetastic.utils import ParametrizedLocator from widgetastic.widget import Checkbox, Text, TextInput, View -from widgetastic_patternfly4.ouia import Button as PF4Button, ExpandableTable +from widgetastic_patternfly import BreadCrumb, Tab +from widgetastic_patternfly4 import Button, Dropdown +from widgetastic_patternfly4.ouia import ( + Button as PF4Button, + ExpandableTable, + PatternflyTable, + Switch, +) -from airgun.views.common import BaseLoggedInView, SearchableViewMixinPF4 +from airgun.views.common import ( + BaseLoggedInView, + SearchableViewMixinPF4, +) +from airgun.widgets import ( + ActionsDropdown, + ConfirmationDialog, + EditableEntry, + PF4ProgressBar, + PF4Search, + ReadOnlyEntry, +) +LOCATION_NUM = 3 -class NewContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): + +class NewAddRemoveResourcesView(View): + searchbox = PF4Search() + type = Dropdown( + locator='.//div[contains(@class, "All repositories") or' + ' contains(@aria-haspopup="listbox")]' + ) + Status = Dropdown( + locator='.//div[contains(@class, "All") or contains(@aria-haspopup="listbox")]' + ) + add_repo = PF4Button('OUIA-Generated-Button-secondary-2') + # Need to add kebab menu + table = PatternflyTable( + component_id='OUIA-Generated-Table-4', + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'Type': Text('.//a'), + 'Name': Text('.//a'), + 'Product': Text('.//a'), + 'Sync State': Text('.//a'), + 'Content': Text('.//a'), + 'Status': Text('.//a'), + }, + ) + + def search(self, value): + """Search for specific available resource and return the results""" + self.searchbox.search(value) + wait_for( + lambda: self.table.is_displayed is True, + timeout=60, + delay=1, + ) + self.table.wait_displayed() + return self.table.read() + + def add(self, value): + """Associate specific resource""" + self.search(value) + next(self.table.rows())[0].widget.fill(True) + self.add_repo.click() + + def fill(self, values): + """Associate resource(s)""" + if not isinstance(values, list): + values = [values] + for value in values: + self.add(value) + + def remove(self, value): + """Unassign some resource(s). + :param str or list values: string containing resource name or a list of + such strings. + """ + self.search(value) + next(self.table.rows())[0].widget.fill(True) + self.remove_button.click() + + def read(self): + """Read all table values from both resource tables""" + return self.table.read() + + +class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): title = Text('.//h1[@data-ouia-component-id="cvPageHeaderText"]') create_content_view = PF4Button('create-content-view') table = ExpandableTable( @@ -18,11 +102,10 @@ class NewContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): @property def is_displayed(self): - assert self.create_content_view.is_displayed() - return True + return self.create_content_view.is_displayed -class NewContentViewCreateView(BaseLoggedInView): +class ContentViewCreateView(BaseLoggedInView): title = Text('.//div[@data-ouia-component-id="create-content-view-modal"]') name = TextInput(id='name') label = TextInput(id='label') @@ -49,10 +132,135 @@ def child_widget_accessed(self, widget): @property def is_displayed(self): - self.title.is_displayed() - self.label.is_displayed() - return True + return self.title.is_displayed def after_fill(self, value): """Ensure 'Create content view' button is enabled after filling out the required fields""" self.submit.wait_displayed() + + +class ContentViewEditView(BaseLoggedInView): + breadcrumb = BreadCrumb('breadcrumbs-list') + search = PF4Search() + actions = ActionsDropdown(".//button[contains(@id, 'toggle-dropdown')]") + publish = PF4Button('cv-details-publish-button') + dialog = ConfirmationDialog() + + @property + def is_displayed(self): + breadcrumb_loaded = self.browser.wait_for_element(self.breadcrumb, exception=False) + return breadcrumb_loaded and self.breadcrumb.locations[0] == 'Content Views' + + @View.nested + class details(Tab): + TAB_LOCATOR = ParametrizedLocator('//a[contains(@href, "#/details")]') + name = EditableEntry(name='Name') + label = ReadOnlyEntry(name='Label') + type = ReadOnlyEntry(name='Composite?') + description = EditableEntry(name='Description') + # depSolv is maybe a conditionalswitch + solve_dependencies = Switch(name='solve_dependencies switch') + import_only = Switch(name='import_only_switch') + + @View.nested + class versions(Tab): + TAB_LOCATOR = ParametrizedLocator('//a[contains(@href, "#/versions")]') + searchbox = PF4Search() + table = PatternflyTable( + component_id="content-view-versions-table", + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'Version': Text('.//a'), + 'Environments': Text('.//a'), + 'Packages': Text('.//a'), + 'Errata': Text('.//a'), + 'Additional content': Text('.//a'), + 'Description': Text('.//a'), + 7: Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]'), + }, + ) + publishButton = PF4Button('cv-details-publish-button') + + def search(self, version_name): + """Searches for content view version. + Searchbox can't search by version name, only by number, that's why in + case version name was passed, it's transformed into recognizable + value before filling, for example - Version 1.0' -> 'version = 1' + """ + search_phrase = version_name + if version_name.startswith('V') and '.' in version_name: + search_phrase = f'version = {version_name.split()[1].split(".")[0]}' + self.searchbox.search(search_phrase) + return self.table.read() + + @View.nested + class content_views(Tab): + TAB_LOCATOR = ParametrizedLocator('//a[contains(@href, "#/contentviews")]') + + resources = View.nested(NewAddRemoveResourcesView) + + @View.nested + class repositories(Tab): + TAB_LOCATOR = ParametrizedLocator('//a[contains(@href, "#/repositories")]') + resources = View.nested(NewAddRemoveResourcesView) + + @View.nested + class filters(Tab): + TAB_LOCATOR = ParametrizedLocator('//a[contains(@href, "#/filters")]') + new_filter = Text(".//button[@ui-sref='content-view.yum.filters.new']") + + +class ContentViewVersionPublishView(BaseLoggedInView): + # publishing view is a popup so adding all navigation within the same context + ROOT = './/div[contains(@class,"pf-c-wizard")]' + title = Text(".//h2[contains(., 'Publish') and contains(@aria-label, 'Publish')]") + # publishing screen + description = TextInput(id='description') + promote = Switch('promote-switch') + + # review screen only has info to review + # shared buttons at bottom for popup for both push and review section + next = Button('Next') + finish = Button('Finish') + back = Button('Back') + cancel = Button('Cancel') + close_button = Button('Close') + progressbar = PF4ProgressBar('.//div[contains(@class, "pf-c-wizard__main-body")]') + + @property + def is_displayed(self): + return self.title.is_displayed + + def wait_animation_end(self): + wait_for( + lambda: 'in' in self.browser.classes(self), + handle_exception=True, + logger=self.logger, + timeout=10, + ) + + def before_fill(self, values=None): + """If we don't want to break view.fill() procedure flow, we need to + push 'Edit' button to open necessary dialog to be able to fill values + """ + self.promote.click() + wait_for( + lambda: self.lce.is_displayed is True, + timeout=30, + delay=1, + logger=self.logger, + ) + + +class NewContentViewVersionDetailsView(BaseLoggedInView): + breadcrumb = BreadCrumb() + + @property + def is_displayed(self): + breadcrumb_loaded = self.browser.wait_for_element(self.breadcrumb, exception=False) + return ( + breadcrumb_loaded + and len(self.breadcrumb.locations) > LOCATION_NUM + and self.breadcrumb.locations[0] == 'Content Views' + and self.breadcrumb.locations[2] == 'Versions' + ) diff --git a/airgun/widgets.py b/airgun/widgets.py index 8d2c48071..fbde0e529 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -27,6 +27,7 @@ VerticalNavigation, ) from widgetastic_patternfly4.ouia import BaseSelect, Button as PF4Button, Dropdown +from widgetastic_patternfly4.progress import Progress as PF4Progress from airgun.exceptions import DisabledWidgetError, ReadOnlyWidgetError from airgun.utils import get_widget_by_name @@ -2106,6 +2107,24 @@ def read(self): return self.progress +class PF4ProgressBar(PF4Progress): + locator = './/div[contains(@class, "pf-c-wizard__main-body")]' + + def wait_for_result(self, timeout=600, delay=1): + """Waits for progress bar to finish. By default checks whether progress + bar is completed every second for 10 minutes. + :param timeout: integer value for timeout in seconds + :param delay: float value for delay between attempts in seconds + """ + wait_for(lambda: self.is_displayed, timeout=30, delay=delay, logger=self.logger) + wait_for( + lambda: not self.is_displayed or self.current_progress == '100', + timeout=timeout, + delay=delay, + logger=self.logger, + ) + + class PublishPromoteProgressBar(ProgressBar): """Progress bar for Publish and Promote procedures. They contain status message and link to associated task. Also the progress is displayed