From 4e365ef94b1bcef94493f2254f35ef05bf1d50d8 Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Wed, 30 Aug 2023 09:30:23 -0500 Subject: [PATCH 1/8] Add minimum elements for Publish workflow --- airgun/entities/contentview_new.py | 55 +++++++++ airgun/views/common.py | 71 ++++++++++++ airgun/views/contentview_new.py | 173 +++++++++++++++++++++++++++++ airgun/widgets.py | 35 ++++++ 4 files changed, 334 insertions(+) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 287156762..47d34e7e2 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -1,4 +1,5 @@ from navmazing import NavigateToSibling +from wait_for import wait_for from airgun.entities.base import BaseEntity from airgun.navigation import NavigateStep @@ -6,6 +7,8 @@ from airgun.utils import retry_navigation from airgun.views.contentview_new import NewContentViewCreateView from airgun.views.contentview_new import NewContentViewTableView +from airgun.views.contentview_new import NewContentViewEditView +from airgun.views.contentview_new import NewContentViewVersionPublishView class NewContentViewEntity(BaseEntity): @@ -22,6 +25,21 @@ def search(self, value): view = self.navigate_to(self, 'All') return view.search(value) + def publish(self, entity_name, values=None): + """Publishes to create new version of CV and promotes the contents to + 'Library' environment. + :return: dict with new content view version table row; contains keys + like 'Version', 'Status', 'Environments' etc. + """ + view = self.navigate_to(self, 'Publish', entity_name=entity_name) + if values: + view.fill(values) + view.next.click() + view.finish.click() + view.progressbar.wait_for_result() + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + return view.versions.table.read() + @navigator.register(NewContentViewEntity, 'All') class ShowAllContentViewsScreen(NavigateStep): @@ -44,3 +62,40 @@ class CreateContentView(NavigateStep): def step(self, *args, **kwargs): self.parent.create_content_view.click() + + +@navigator.register(NewContentViewEntity, 'Edit') +class EditContentView(NavigateStep): + """Navigate to Edit Content View screen. + Args: + entity_name: name of content view + """ + + VIEW = NewContentViewEditView + + 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. + Args: + entity_name: name of content view + """ + + VIEW = NewContentViewVersionPublishView + + def prerequisite(self, *args, **kwargs): + """Open Content View first.""" + return self.navigate_to(self.obj, 'Edit', entity_name=kwargs.get('entity_name')) + + 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 dcb74e5ef..08915448f 100644 --- a/airgun/views/common.py +++ b/airgun/views/common.py @@ -1,3 +1,7 @@ +import time + +from wait_for import wait_for + from widgetastic.widget import Checkbox from widgetastic.widget import ConditionalSwitchableView from widgetastic.widget import do_not_read_this_widget @@ -13,6 +17,8 @@ from widgetastic_patternfly import TabWithDropdown from widgetastic_patternfly4.navigation import Navigation from widgetastic_patternfly4.ouia import Dropdown +from widgetastic_patternfly4.ouia import PatternflyTable +from widgetastic_patternfly4.ouia import Button as PF4Button from airgun.utils import get_widget_by_name from airgun.utils import normalize_dict_values @@ -383,6 +389,71 @@ 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 = list((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 71c64d558..e75088c1d 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -1,9 +1,37 @@ +from wait_for import wait_for +from widgetastic.utils import ParametrizedLocator from widgetastic.widget import Checkbox +from widgetastic.widget import ParametrizedView from widgetastic.widget import Text from widgetastic.widget import TextInput from widgetastic.widget import View +from widgetastic_patternfly import BreadCrumb +from widgetastic_patternfly import Tab +from widgetastic_patternfly4 import Button +from widgetastic_patternfly4 import Dropdown from widgetastic_patternfly4.ouia import Button as PF4Button from widgetastic_patternfly4.ouia import ExpandableTable +from widgetastic_patternfly4.ouia import Switch +from widgetastic_patternfly4.ouia import PatternflyTable + +from airgun.views.common import BaseLoggedInView +from airgun.views.common import NewAddRemoveResourcesView +from airgun.views.common import SearchableViewMixinPF4 +from airgun.widgets import ActionsDropdown +from airgun.widgets import ConfirmationDialog +from airgun.widgets import EditableEntry +from airgun.widgets import PF4Search +from airgun.widgets import ProgressBarPF4 +from airgun.widgets import ReadOnlyEntry +from airgun.views.common import BaseLoggedInView +from airgun.views.common import NewAddRemoveResourcesView +from airgun.views.common import SearchableViewMixinPF4 +from airgun.widgets import ActionsDropdown +from airgun.widgets import ConfirmationDialog +from airgun.widgets import EditableEntry +from airgun.widgets import PF4Search +from airgun.widgets import ProgressBarPF4 +from airgun.widgets import ReadOnlyEntry from airgun.views.common import BaseLoggedInView from airgun.views.common import SearchableViewMixinPF4 @@ -61,3 +89,148 @@ def is_displayed(self): def after_fill(self, value): """Ensure 'Create content view' button is enabled after filling out the required fields""" self.submit.wait_displayed() + + +class NewContentViewEditView(BaseLoggedInView): + breadcrumb = BreadCrumb() + search = PF4Search() + title = Text("//h2[contains(., 'Publish) or contains(@id, 'pf-wizard-title-0')]") + actions = ActionsDropdown( + "//div[contains(@data-ouia-component-id, 'OUIA-Generated-Dropdown-2')]" + ) + publish = PF4Button('cv-details-publish-button') + # not sure if this is needed + dialog = ConfirmationDialog() + + @property + def is_displayed(self): + breadcrumb_loaded = self.browser.wait_for_element(self.breadcrumb, exception=False) + return ( + breadcrumb_loaded + and len(self.breadcrumb.locations) <= 3 + and self.breadcrumb.locations[0] == 'Content Views' + and self.breadcrumb.read() != 'New Content View' + and self.publish.is_displayed + ) + + @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 id, 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 NewContentViewVersionPublishView(BaseLoggedInView): + # publishing view is a popup so adding all navigation within the same context + breadcrumb = BreadCrumb() + ROOT = './/div[contains(@class,"pf-c-wizard")]' + title = Text("//h2[contains(., 'Publish' or contains(@id, 'pf-wizard-title-0')]") + # 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 = ProgressBarPF4() + + @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' + and self.breadcrumb.read() == 'Versions' + ) + + 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) > 3 + and self.breadcrumb.locations[0] == 'Content Views' + and self.breadcrumb.locations[2] == 'Versions' + ) + diff --git a/airgun/widgets.py b/airgun/widgets.py index fafbc270f..af189d1dc 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -2034,6 +2034,41 @@ def read(self): return self.progress +class ProgressBarPF4(ProgressBar): + """Generic progress bar widget. + Example html representation:: +
+
+
+ Locator example:: + .//div[contains(@class, "progress progress-striped")] + """ + + PROGRESSBAR = '//div[contains(@role, "progressbar") or contains(@class, "pf-c-progress__bar")]' + + def __init__(self, parent, locator=None, logger=None): + """Provide common progress bar locator if it wasn't specified.""" + Widget.__init__(self, parent, logger=logger) + if not locator: + locator = self.PROGRESSBAR + self.locator = locator + + @property + def progress(self): + """String value with current flow rate in percent.""" + return self.browser.get_attribute( + 'pf-c-progress__measure', self.PROGRESSBAR, check_safe=False + ) + + @property + def is_completed(self): + """Boolean value whether progress bar is finished or not""" + if not self.is_active and self.progress == '100%': + return True + return False + + class PublishPromoteProgressBar(ProgressBar): """Progress bar for Publish and Promote procedures. They contain status message and link to associated task. Also the progress is displayed From daf0f03a4365663ac056a743f85301f166f0e10f Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Tue, 5 Sep 2023 22:18:28 -0500 Subject: [PATCH 2/8] Address review comments --- airgun/entities/contentview_new.py | 16 ++++++++-------- airgun/views/contentview_new.py | 14 +++++++------- airgun/widgets.py | 16 +--------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 47d34e7e2..5cf21b070 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -5,10 +5,10 @@ from airgun.navigation import NavigateStep from airgun.navigation import navigator from airgun.utils import retry_navigation -from airgun.views.contentview_new import NewContentViewCreateView -from airgun.views.contentview_new import NewContentViewTableView -from airgun.views.contentview_new import NewContentViewEditView -from airgun.views.contentview_new import NewContentViewVersionPublishView +from airgun.views.contentview_new import ContentViewCreateView +from airgun.views.contentview_new import ContentViewTableView +from airgun.views.contentview_new import ContentViewEditView +from airgun.views.contentview_new import ContentViewVersionPublishView class NewContentViewEntity(BaseEntity): @@ -45,7 +45,7 @@ def publish(self, entity_name, values=None): class ShowAllContentViewsScreen(NavigateStep): """Navigate to All Content Views screen.""" - VIEW = NewContentViewTableView + VIEW = ContentViewTableView @retry_navigation def step(self, *args, **kwargs): @@ -56,7 +56,7 @@ def step(self, *args, **kwargs): class CreateContentView(NavigateStep): """Navigate to Create content view.""" - VIEW = NewContentViewCreateView + VIEW = ContentViewCreateView prerequisite = NavigateToSibling('All') @@ -71,7 +71,7 @@ class EditContentView(NavigateStep): entity_name: name of content view """ - VIEW = NewContentViewEditView + VIEW = ContentViewEditView def prerequisite(self, *args, **kwargs): return self.navigate_to(self.obj, 'All') @@ -89,7 +89,7 @@ class PublishContentViewVersion(NavigateStep): entity_name: name of content view """ - VIEW = NewContentViewVersionPublishView + VIEW = ContentViewVersionPublishView def prerequisite(self, *args, **kwargs): """Open Content View first.""" diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py index e75088c1d..12f88b607 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -21,7 +21,7 @@ from airgun.widgets import ConfirmationDialog from airgun.widgets import EditableEntry from airgun.widgets import PF4Search -from airgun.widgets import ProgressBarPF4 +from airgun.widgets import PF4ProgressBar from airgun.widgets import ReadOnlyEntry from airgun.views.common import BaseLoggedInView from airgun.views.common import NewAddRemoveResourcesView @@ -30,14 +30,14 @@ from airgun.widgets import ConfirmationDialog from airgun.widgets import EditableEntry from airgun.widgets import PF4Search -from airgun.widgets import ProgressBarPF4 +from airgun.widgets import PF4ProgressBar from airgun.widgets import ReadOnlyEntry from airgun.views.common import BaseLoggedInView from airgun.views.common import SearchableViewMixinPF4 -class NewContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): +class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): title = Text('.//h1[@data-ouia-component-id="cvPageHeaderText"]') create_content_view = PF4Button('create-content-view') table = ExpandableTable( @@ -55,7 +55,7 @@ def is_displayed(self): return True -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') @@ -91,7 +91,7 @@ def after_fill(self, value): self.submit.wait_displayed() -class NewContentViewEditView(BaseLoggedInView): +class ContentViewEditView(BaseLoggedInView): breadcrumb = BreadCrumb() search = PF4Search() title = Text("//h2[contains(., 'Publish) or contains(@id, 'pf-wizard-title-0')]") @@ -173,7 +173,7 @@ class filters(Tab): new_filter = Text(".//button[@ui-sref='content-view.yum.filters.new']") -class NewContentViewVersionPublishView(BaseLoggedInView): +class ContentViewVersionPublishView(BaseLoggedInView): # publishing view is a popup so adding all navigation within the same context breadcrumb = BreadCrumb() ROOT = './/div[contains(@class,"pf-c-wizard")]' @@ -189,7 +189,7 @@ class NewContentViewVersionPublishView(BaseLoggedInView): back = Button('Back') cancel = Button('Cancel') close_button = Button('Close') - progressbar = ProgressBarPF4() + progressbar = PF4ProgressBar() @property def is_displayed(self): diff --git a/airgun/widgets.py b/airgun/widgets.py index af189d1dc..10f7ebc64 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -2034,7 +2034,7 @@ def read(self): return self.progress -class ProgressBarPF4(ProgressBar): +class PF4ProgressBar(ProgressBar): """Generic progress bar widget. Example html representation::
@@ -2047,13 +2047,6 @@ class ProgressBarPF4(ProgressBar): PROGRESSBAR = '//div[contains(@role, "progressbar") or contains(@class, "pf-c-progress__bar")]' - def __init__(self, parent, locator=None, logger=None): - """Provide common progress bar locator if it wasn't specified.""" - Widget.__init__(self, parent, logger=logger) - if not locator: - locator = self.PROGRESSBAR - self.locator = locator - @property def progress(self): """String value with current flow rate in percent.""" @@ -2061,13 +2054,6 @@ def progress(self): 'pf-c-progress__measure', self.PROGRESSBAR, check_safe=False ) - @property - def is_completed(self): - """Boolean value whether progress bar is finished or not""" - if not self.is_active and self.progress == '100%': - return True - return False - class PublishPromoteProgressBar(ProgressBar): """Progress bar for Publish and Promote procedures. They contain status From b33e81dacc1bf9c12de58a5d538b0bbbda514e2e Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Mon, 18 Sep 2023 13:06:46 -0500 Subject: [PATCH 3/8] Add french navigation --- airgun/entities/contentview_new.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 323a961ad..91495a84d 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -39,6 +39,9 @@ def publish(self, entity_name, values=None): view = self.navigate_to(self, 'Edit', entity_name=entity_name) return view.versions.table.read() + def check_if_blank_in_french(self): + view = self.navigate_to(self, 'French') + return view.table.read() @navigator.register(NewContentViewEntity, 'All') class ShowAllContentViewsScreen(NavigateStep): @@ -51,6 +54,17 @@ def step(self, *args, **kwargs): self.view.menu.select('Content', 'Lifecycle', 'Content Views') +@navigator.register(NewContentViewEntity, 'French') +class ShowAllContentViewsScreenFrench(NavigateStep): + """Navigate to All Content Views screen ( in French )""" + + VIEW = ContentViewTableView + + @retry_navigation + def step(self, *args, **kwargs): + self.view.menu.select('Contenu', 'Lifecycle', 'Content Views') + + @navigator.register(NewContentViewEntity, 'New') class CreateContentView(NavigateStep): """Navigate to Create content view.""" From 11627cda711a48e87f001e186c2a33710d6c8d5a Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Mon, 9 Oct 2023 12:05:48 -0500 Subject: [PATCH 4/8] Change entity name, add some checks --- airgun/entities/contentview_new.py | 25 ++++++++---- airgun/views/contentview_new.py | 64 +++++++++++++----------------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 91495a84d..85d4dbd01 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -1,13 +1,14 @@ from navmazing import NavigateToSibling -from wait_for import wait_for from airgun.entities.base import BaseEntity from airgun.navigation import NavigateStep, navigator from airgun.utils import retry_navigation -from airgun.views.contentview_new import ContentViewCreateView -from airgun.views.contentview_new import ContentViewTableView -from airgun.views.contentview_new import ContentViewEditView -from airgun.views.contentview_new import ContentViewVersionPublishView +from airgun.views.contentview_new import ( + ContentViewCreateView, + ContentViewEditView, + ContentViewTableView, + ContentViewVersionPublishView, +) class NewContentViewEntity(BaseEntity): @@ -16,33 +17,42 @@ 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 to create new version of CV and promotes the contents to 'Library' environment. :return: dict with new content view version table row; contains keys - like 'Version', 'Status', 'Environments' etc. + like 'Version', 'Status', 'Environments' etc. """ 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() view = self.navigate_to(self, 'Edit', entity_name=entity_name) + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() return view.versions.table.read() - def check_if_blank_in_french(self): + def read_french_lang_cv(self): view = self.navigate_to(self, 'French') return view.table.read() + @navigator.register(NewContentViewEntity, 'All') class ShowAllContentViewsScreen(NavigateStep): """Navigate to All Content Views screen.""" @@ -111,4 +121,3 @@ def prerequisite(self, *args, **kwargs): def step(self, *args, **kwargs): """Click 'Publish new version' button""" self.parent.publish.click() - diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py index 323a5ef0b..7ea6ae836 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -1,39 +1,30 @@ from wait_for import wait_for from widgetastic.utils import ParametrizedLocator -from widgetastic.widget import Checkbox -from widgetastic.widget import ParametrizedView -from widgetastic.widget import Text -from widgetastic.widget import TextInput -from widgetastic.widget import View -from widgetastic_patternfly import BreadCrumb -from widgetastic_patternfly import Tab -from widgetastic_patternfly4 import Button -from widgetastic_patternfly4 import Dropdown -from widgetastic_patternfly4.ouia import Button as PF4Button -from widgetastic_patternfly4.ouia import ExpandableTable -from widgetastic_patternfly4.ouia import Switch -from widgetastic_patternfly4.ouia import PatternflyTable - -from airgun.views.common import BaseLoggedInView -from airgun.views.common import NewAddRemoveResourcesView -from airgun.views.common import SearchableViewMixinPF4 -from airgun.widgets import ActionsDropdown -from airgun.widgets import ConfirmationDialog -from airgun.widgets import EditableEntry -from airgun.widgets import PF4Search -from airgun.widgets import PF4ProgressBar -from airgun.widgets import ReadOnlyEntry -from airgun.views.common import BaseLoggedInView -from airgun.views.common import NewAddRemoveResourcesView -from airgun.views.common import SearchableViewMixinPF4 -from airgun.widgets import ActionsDropdown -from airgun.widgets import ConfirmationDialog -from airgun.widgets import EditableEntry -from airgun.widgets import PF4Search -from airgun.widgets import PF4ProgressBar -from airgun.widgets import ReadOnlyEntry - -from airgun.views.common import BaseLoggedInView, SearchableViewMixinPF4 +from widgetastic.widget import Checkbox, Text, TextInput, View +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, + NewAddRemoveResourcesView, + SearchableViewMixinPF4, +) +from airgun.widgets import ( + ActionsDropdown, + ConfirmationDialog, + EditableEntry, + PF4ProgressBar, + PF4Search, + ReadOnlyEntry, +) + +LOCATION_NUM = 3 class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): @@ -106,7 +97,7 @@ def is_displayed(self): breadcrumb_loaded = self.browser.wait_for_element(self.breadcrumb, exception=False) return ( breadcrumb_loaded - and len(self.breadcrumb.locations) <= 3 + and len(self.breadcrumb.locations) <= LOCATION_NUM and self.breadcrumb.locations[0] == 'Content Views' and self.breadcrumb.read() != 'New Content View' and self.publish.is_displayed @@ -228,8 +219,7 @@ def is_displayed(self): breadcrumb_loaded = self.browser.wait_for_element(self.breadcrumb, exception=False) return ( breadcrumb_loaded - and len(self.breadcrumb.locations) > 3 + and len(self.breadcrumb.locations) > LOCATION_NUM and self.breadcrumb.locations[0] == 'Content Views' and self.breadcrumb.locations[2] == 'Versions' ) - From bc9d0a4ffa8dfa30386fb6aba78d814fffb4a701 Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Mon, 9 Oct 2023 15:09:51 -0500 Subject: [PATCH 5/8] Rework progressbar widget to use PF4 widget --- airgun/entities/contentview_new.py | 4 +++- airgun/views/contentview_new.py | 25 ++++++---------------- airgun/widgets.py | 33 +++++++++++++++--------------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 85d4dbd01..61c3c9dd2 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -42,7 +42,7 @@ def publish(self, entity_name, values=None): view.fill(values) view.next.click() view.finish.click() - view.progressbar.wait_for_result() + view.progressbar.wait_for_result(delay=.01) view = self.navigate_to(self, 'Edit', entity_name=entity_name) self.browser.plugin.ensure_page_safe(timeout='5s') view.wait_displayed() @@ -50,6 +50,8 @@ def publish(self, entity_name, values=None): def read_french_lang_cv(self): view = self.navigate_to(self, 'French') + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() return view.table.read() diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py index 7ea6ae836..950fed0d1 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -41,8 +41,7 @@ class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): @property def is_displayed(self): - assert self.create_content_view.is_displayed() - return True + return self.create_content_view.is_displayed() class ContentViewCreateView(BaseLoggedInView): @@ -82,7 +81,7 @@ def after_fill(self, value): class ContentViewEditView(BaseLoggedInView): - breadcrumb = BreadCrumb() + breadcrumb = BreadCrumb('breadcrumbs-list') search = PF4Search() title = Text("//h2[contains(., 'Publish) or contains(@id, 'pf-wizard-title-0')]") actions = ActionsDropdown( @@ -95,13 +94,7 @@ class ContentViewEditView(BaseLoggedInView): @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.read() != 'New Content View' - and self.publish.is_displayed - ) + return breadcrumb_loaded and self.breadcrumb.locations[0] == 'Content Views' @View.nested class details(Tab): @@ -165,9 +158,8 @@ class filters(Tab): class ContentViewVersionPublishView(BaseLoggedInView): # publishing view is a popup so adding all navigation within the same context - breadcrumb = BreadCrumb() ROOT = './/div[contains(@class,"pf-c-wizard")]' - title = Text("//h2[contains(., 'Publish' or contains(@id, 'pf-wizard-title-0')]") + title = Text(".//h2[contains(., 'Publish') and contains(@aria-label, 'Publish')]") # publishing screen description = TextInput(id='description') promote = Switch('promote-switch') @@ -179,16 +171,11 @@ class ContentViewVersionPublishView(BaseLoggedInView): back = Button('Back') cancel = Button('Cancel') close_button = Button('Close') - progressbar = PF4ProgressBar() + progressbar = PF4ProgressBar('.//div[contains(@class, "pf-c-wizard__main-body")]') @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' - and self.breadcrumb.read() == 'Versions' - ) + return self.title.wait_displayed() def wait_animation_end(self): wait_for( diff --git a/airgun/widgets.py b/airgun/widgets.py index 33f41d285..48c96adce 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -28,6 +28,8 @@ ) 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 @@ -2030,24 +2032,23 @@ def read(self): return self.progress -class PF4ProgressBar(ProgressBar): - """Generic progress bar widget. - Example html representation:: -
-
-
- Locator example:: - .//div[contains(@class, "progress progress-striped")] - """ +class PF4ProgressBar(PF4Progress): - PROGRESSBAR = '//div[contains(@role, "progressbar") or contains(@class, "pf-c-progress__bar")]' + locator = './/div[contains(@class, "pf-c-wizard__main-body")]' - @property - def progress(self): - """String value with current flow rate in percent.""" - return self.browser.get_attribute( - 'pf-c-progress__measure', self.PROGRESSBAR, check_safe=False + 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, ) From ce6398509dd67efeeb769a2b716fab609dff8184 Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Mon, 9 Oct 2023 15:31:08 -0500 Subject: [PATCH 6/8] address review comments --- airgun/views/common.py | 68 -------------------------------- airgun/views/contentview_new.py | 70 ++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 73 deletions(-) diff --git a/airgun/views/common.py b/airgun/views/common.py index f710db25d..55f0a4432 100644 --- a/airgun/views/common.py +++ b/airgun/views/common.py @@ -1,4 +1,3 @@ -import time from widgetastic.widget import ( Checkbox, ConditionalSwitchableView, @@ -13,8 +12,6 @@ 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 PatternflyTable -from widgetastic_patternfly4.ouia import Button as PF4Button from airgun.utils import get_widget_by_name, normalize_dict_values from airgun.widgets import ( @@ -386,71 +383,6 @@ 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 = list((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 950fed0d1..4409ada76 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -12,7 +12,6 @@ from airgun.views.common import ( BaseLoggedInView, - NewAddRemoveResourcesView, SearchableViewMixinPF4, ) from airgun.widgets import ( @@ -26,6 +25,67 @@ LOCATION_NUM = 3 +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 = list((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"]') @@ -83,12 +143,10 @@ def after_fill(self, value): class ContentViewEditView(BaseLoggedInView): breadcrumb = BreadCrumb('breadcrumbs-list') search = PF4Search() - title = Text("//h2[contains(., 'Publish) or contains(@id, 'pf-wizard-title-0')]") actions = ActionsDropdown( - "//div[contains(@data-ouia-component-id, 'OUIA-Generated-Dropdown-2')]" + ".//button[contains(@id, 'toggle-dropdown')]" ) publish = PF4Button('cv-details-publish-button') - # not sure if this is needed dialog = ConfirmationDialog() @property @@ -128,7 +186,7 @@ class versions(Tab): def search(self, version_name): """Searches for content view version. - Searchbox can't search by version name, only by id, that's why in + 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' @@ -210,3 +268,5 @@ def is_displayed(self): and self.breadcrumb.locations[0] == 'Content Views' and self.breadcrumb.locations[2] == 'Versions' ) + + From 1cd34efb8606343ab409d0fd21608b57658f7143 Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Tue, 10 Oct 2023 09:12:41 -0500 Subject: [PATCH 7/8] Fix ruff issues --- airgun/entities/contentview_new.py | 19 +++++-------------- airgun/views/contentview_new.py | 14 +++++--------- airgun/widgets.py | 2 -- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 61c3c9dd2..a21c0b2ce 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -30,11 +30,7 @@ def search(self, value): return view.search(value) def publish(self, entity_name, values=None): - """Publishes to create new version of CV and promotes the contents to - 'Library' environment. - :return: dict with new content view version table row; contains keys - like 'Version', 'Status', 'Environments' etc. - """ + """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() @@ -42,13 +38,14 @@ def publish(self, entity_name, values=None): view.fill(values) view.next.click() view.finish.click() - view.progressbar.wait_for_result(delay=.01) + 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() return view.versions.table.read() def read_french_lang_cv(self): + """Navigates to main CV page, when system is set to French, and reads table""" view = self.navigate_to(self, 'French') self.browser.plugin.ensure_page_safe(timeout='5s') view.wait_displayed() @@ -91,10 +88,7 @@ def step(self, *args, **kwargs): @navigator.register(NewContentViewEntity, 'Edit') class EditContentView(NavigateStep): - """Navigate to Edit Content View screen. - Args: - entity_name: name of content view - """ + """Navigate to Edit Content View screen.""" VIEW = ContentViewEditView @@ -109,10 +103,7 @@ def step(self, *args, **kwargs): @navigator.register(NewContentViewEntity, 'Publish') class PublishContentViewVersion(NavigateStep): - """Navigate to Content View Publish screen. - Args: - entity_name: name of content view - """ + """Navigate to Content View Publish screen.""" VIEW = ContentViewVersionPublishView diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py index 4409ada76..4a559bf0d 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -25,6 +25,7 @@ LOCATION_NUM = 3 + class NewAddRemoveResourcesView(View): searchbox = PF4Search() type = Dropdown( @@ -69,14 +70,14 @@ def add(self, value): def fill(self, values): """Associate resource(s)""" if not isinstance(values, list): - values = list((values,)) + 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. + such strings. """ self.search(value) next(self.table.rows())[0].widget.fill(True) @@ -143,9 +144,7 @@ def after_fill(self, value): class ContentViewEditView(BaseLoggedInView): breadcrumb = BreadCrumb('breadcrumbs-list') search = PF4Search() - actions = ActionsDropdown( - ".//button[contains(@id, 'toggle-dropdown')]" - ) + actions = ActionsDropdown(".//button[contains(@id, 'toggle-dropdown')]") publish = PF4Button('cv-details-publish-button') dialog = ConfirmationDialog() @@ -188,8 +187,7 @@ 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' + value before filling, for example - Version 1.0' -> 'version = 1' """ search_phrase = version_name if version_name.startswith('V') and '.' in version_name: @@ -268,5 +266,3 @@ def is_displayed(self): and self.breadcrumb.locations[0] == 'Content Views' and self.breadcrumb.locations[2] == 'Versions' ) - - diff --git a/airgun/widgets.py b/airgun/widgets.py index 48c96adce..5a4cf5855 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -27,7 +27,6 @@ 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 @@ -2033,7 +2032,6 @@ def read(self): class PF4ProgressBar(PF4Progress): - locator = './/div[contains(@class, "pf-c-wizard__main-body")]' def wait_for_result(self, timeout=600, delay=1): From ef2219d5d3effb3aa48a5c439f46ffa28fa775ef Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Mon, 16 Oct 2023 13:15:31 -0500 Subject: [PATCH 8/8] Fix is_displayed calls --- airgun/views/contentview_new.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py index 4a559bf0d..4d103a531 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -102,7 +102,7 @@ class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): @property def is_displayed(self): - return self.create_content_view.is_displayed() + return self.create_content_view.is_displayed class ContentViewCreateView(BaseLoggedInView): @@ -132,9 +132,7 @@ 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"""