From 80c3d8495b6e994c6c42128c7e22fffb758f35c8 Mon Sep 17 00:00:00 2001 From: Sam Bible Date: Thu, 5 Sep 2024 23:21:51 -0500 Subject: [PATCH] Large changes to make publish/promote/composite/add content work --- airgun/entities/contentview_new.py | 131 ++++++++++++++++++++++++----- airgun/views/common.py | 121 ++++++++++++++------------ airgun/views/contentview_new.py | 124 +++++++++++---------------- airgun/widgets.py | 7 ++ 4 files changed, 234 insertions(+), 149 deletions(-) diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py index 6007da6c0..419071d85 100644 --- a/airgun/entities/contentview_new.py +++ b/airgun/entities/contentview_new.py @@ -1,3 +1,5 @@ +import time + from navmazing import NavigateToSibling from widgetastic.exceptions import NoSuchElementException @@ -5,11 +7,13 @@ from airgun.navigation import NavigateStep, navigator from airgun.utils import retry_navigation from airgun.views.contentview_new import ( + AddContentViewModal, AddRPMRuleView, ContentViewCreateView, ContentViewEditView, ContentViewTableView, ContentViewVersionDetailsView, + ContentViewVersionPromoteView, ContentViewVersionPublishView, CreateFilterView, EditFilterView, @@ -19,11 +23,13 @@ class NewContentViewEntity(BaseEntity): endpoint_path = '/content_views' - def create(self, values): + def create(self, values, composite=False): """Create a new content view""" view = self.navigate_to(self, 'New') self.browser.plugin.ensure_page_safe(timeout='5s') view.wait_displayed() + if composite: + view.composite_tile.click() view.fill(values) view.submit.click() @@ -34,20 +40,57 @@ def search(self, value): view.wait_displayed() return view.search(value) - def publish(self, entity_name, values=None): - """Publishes new version of CV""" + def publish(self, entity_name, values=None, promote=False, lce=None): + """Publishes new version of CV, optionally allowing for instant promotion""" 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() + if promote: + view.promote.click() + view.lce_selector.fill({lce: True}) + view.next_button.click() + view.finish_button.click() 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 add_content(self, entity_name, content_name): + """Add specified content to the given Content View""" + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() + view.repositories.resources.add(content_name) + return view.repositories.resources.read() + + def add_cv(self, ccv_name, cv_name, always_update=False, version=None): + """Adds selected CV to selected CCV, optionally with support for always_update and specified version""" + view = self.navigate_to(self, 'Edit', entity_name=ccv_name) + self.browser.plugin.ensure_page_safe(timeout='5s') + view.wait_displayed() + view.content_views.resources.add(cv_name) + view = AddContentViewModal(self.browser) + if always_update: + view.always_update.fill(True) + if version: + view.version_select.item_select(version) + view.submit_button.click() + view = self.navigate_to(self, 'Edit', entity_name=ccv_name) + return view.content_views.resources.read() + + def read_cv(self, entity_name, version_name): + """Reads the table for a specified Content View's specified Version""" + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + view.versions.search(version_name) + return view.versions.table.row(version=version_name).read() + + def read_repositories(self, entity_name): + """Reads the repositories table for a specified Content View""" + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + return view.repositories.resources.read() + def read_version_table(self, entity_name, version, tab_name, search_param=None): """Reads a specific table for a CV Version""" view = self.navigate_to(self, 'Version', entity_name=entity_name, version=version) @@ -129,6 +172,31 @@ def read_french_lang_cv(self): view.wait_displayed() return view.table.read() + def promote(self, entity_name, version_name, lce_name): + """Promotes the selected version of content view to given environment. + :return: dict with new content view version table row; contains keys + like 'Version', 'Status', 'Environments' etc. + """ + view = self.navigate_to(self, 'Promote', entity_name=entity_name, version_name=version_name) + modal = ContentViewVersionPromoteView(self.browser) + if modal.is_displayed: + modal.lce.fill({lce_name: True}) + modal.promote_btn.click() + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + view.versions.search(version_name) + return view.versions.table.row(version=version_name).read() + + def update(self, entity_name, values): + """Update existing content view""" + view = self.navigate_to(self, 'Edit', entity_name=entity_name) + # need a wait to recognize the loading is complete + # sleep works for now + time.sleep(3) + filled_values = view.fill(values) + view.flash.assert_no_error() + view.flash.dismiss() + return filled_values + @navigator.register(NewContentViewEntity, 'All') class ShowAllContentViewsScreen(NavigateStep): @@ -164,22 +232,6 @@ def step(self, *args, **kwargs): self.parent.create_content_view.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() - - @navigator.register(NewContentViewEntity, 'Edit') class EditContentView(NavigateStep): """Navigate to Edit Content View screen.""" @@ -208,3 +260,40 @@ def step(self, *args, **kwargs): version = kwargs.get('version') self.parent.versions.search(version) self.parent.versions.table.row(version=version)['Version'].widget.click() + + +@navigator.register(NewContentViewEntity, 'Publish') +class PublishContentViewVersion(NavigateStep): + """Navigate to Content View Publish screen. + Args: + entity_name: name of content view + """ + + VIEW = ContentViewVersionPublishView + + 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() + + +@navigator.register(NewContentViewEntity, 'Promote') +class PromoteContentViewVersion(NavigateStep): + """Navigate to Content View Promote screen. + Args: + entity_name: name of content view + version_name: name of content view version to promote + """ + + VIEW = ContentViewEditView + + def prerequisite(self, *args, **kwargs): + return self.navigate_to(self.obj, 'Edit', entity_name=kwargs.get('entity_name')) + + def step(self, *args, **kwargs): + version_name = kwargs.get('version_name') + self.parent.versions.search(version_name) + self.parent.versions.table[0][7].widget.item_select('Promote') diff --git a/airgun/views/common.py b/airgun/views/common.py index 2c222dc40..2419e9428 100644 --- a/airgun/views/common.py +++ b/airgun/views/common.py @@ -1,6 +1,3 @@ -import time - -import wait_for from widgetastic.widget import ( Checkbox, ConditionalSwitchableView, @@ -12,9 +9,10 @@ WTMixin, do_not_read_this_widget, ) -from widgetastic_patternfly import BreadCrumb, Button, Tab, TabWithDropdown +from widgetastic_patternfly import BreadCrumb, Tab, TabWithDropdown +from widgetastic_patternfly4 import Button, Select from widgetastic_patternfly4.navigation import Navigation -from widgetastic_patternfly4.ouia import Button as PF4Button, Dropdown, PatternflyTable +from widgetastic_patternfly4.ouia import Dropdown, PatternflyTable from airgun.utils import get_widget_by_name, normalize_dict_values from airgun.widgets import ( @@ -25,6 +23,7 @@ ItemsList, LCESelector, Pf4ConfirmationDialog, + PF4LCECheckSelector, PF4LCESelector, PF4NavSearch, PF4Search, @@ -285,6 +284,30 @@ class PF4LCESelectorGroup(LCESelectorGroup): ) +class PF4LCECheckSelectorGroup(PF4LCESelectorGroup): + """Checkbox version of PF4 LCE Selector""" + + lce = PF4LCECheckSelector( + locator=ParametrizedLocator( + './/div[@class="env-path" and .//*[contains(normalize-space(.), "{lce_name}")]]' + ) + ) + + +class PF4LCEGroup(ParametrizedLocator): + "Group of LCE indicators" + ROOT = './/td and ' + + PARAMETERS = ('lce_name',) + + LAST_ENV = './/div[@class="env-path"][last()]' + lce = PF4LCESelector( + locator=ParametrizedLocator( + './/div[@class="env-path" and .//*[contains(normalize-space(.), "{lce_name}")]]' + ) + ) + + class ListRemoveTab(SatSecondaryTab): """'List/Remove' tab, part of :class:`AddRemoveResourcesView`.""" @@ -390,38 +413,13 @@ def read(self): } -class AddRemoveSubscriptionsView(AddRemoveResourcesView): - """A variant of :class:`AddRemoveResourcesView` for managing subscriptions. - Subscriptions table has different structure - entity label is located in - separate row apart from checkbox and other cells. - """ - - @View.nested - class list_remove_tab(ListRemoveTab): - table = SatSubscriptionsTable( - locator=".//table", column_widgets={0: Checkbox(locator=".//input[@type='checkbox']")} - ) - - @View.nested - class add_tab(AddTab): - table = SatSubscriptionsTable( - locator=".//table", column_widgets={0: Checkbox(locator=".//input[@type='checkbox']")} - ) - - 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 + status = Select(locator='.//div[@data-ouia-component-id="select Status"]') + remove_button = Dropdown(locator='.//div[@data-ouia-component-id="repositoies-bulk-actions"]') + add_button = Button(locator='.//button[@data-ouia-component-id="add-repositories"]') table = PatternflyTable( - component_id='OUIA-Generated-Table-4', + component_id='content-view-repositories-table', column_widgets={ 0: Checkbox(locator='.//input[@type="checkbox"]'), 'Type': Text('.//a'), @@ -433,49 +431,68 @@ class NewAddRemoveResourcesView(View): }, ) + def select_status(self, value): + """Set status box to passed in value""" + self.status.fill(value) + 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() + return self.read() def add(self, value): """Associate specific resource""" + self.select_status("Not added") self.search(value) + value = self.table.rows() next(self.table.rows())[0].widget.fill(True) - self.add_repo.click() + self.add_button.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. + :param str or list values: string containing resource name or a list of such strings. """ + self.select_status("Added") self.search(value) next(self.table.rows())[0].widget.fill(True) - self.remove_button.click() + self.remove_button.item_select('Remove') def read(self): """Read all table values from both resource tables""" + self.browser.wait_for_element(locator='//h4[text()="Loading"]', exception=False) + self.browser.wait_for_element( + self.table, exception=False, ensure_page_safe=True, timeout=10 + ) + self.browser.plugin.ensure_page_safe(timeout='60s') + self.table.wait_displayed() + self.select_status("All") return self.table.read() +class AddRemoveSubscriptionsView(AddRemoveResourcesView): + """A variant of :class:`AddRemoveResourcesView` for managing subscriptions. + Subscriptions table has different structure - entity label is located in + separate row apart from checkbox and other cells. + """ + + @View.nested + class list_remove_tab(ListRemoveTab): + table = SatSubscriptionsTable( + locator=".//table", column_widgets={0: Checkbox(locator=".//input[@type='checkbox']")} + ) + + @View.nested + class add_tab(AddTab): + table = SatSubscriptionsTable( + locator=".//table", column_widgets={0: Checkbox(locator=".//input[@type='checkbox']")} + ) + + 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 c6db5fbdb..12c955f2c 100644 --- a/airgun/views/contentview_new.py +++ b/airgun/views/contentview_new.py @@ -1,8 +1,8 @@ from wait_for import wait_for from widgetastic.utils import ParametrizedLocator -from widgetastic.widget import Checkbox, Text, TextInput, View +from widgetastic.widget import Checkbox, ParametrizedView, Text, TextInput, View from widgetastic_patternfly import BreadCrumb, Tab -from widgetastic_patternfly4 import Button, Dropdown, Radio as PF4Radio +from widgetastic_patternfly4 import Button, Dropdown, Modal, Radio as PF4Radio, Select from widgetastic_patternfly4.ouia import ( Button as PF4Button, ExpandableTable, @@ -13,6 +13,9 @@ from airgun.views.common import ( BaseLoggedInView, + NewAddRemoveResourcesView, + PF4LCECheckSelectorGroup, + PF4LCESelectorGroup, SearchableViewMixinPF4, ) from airgun.widgets import ( @@ -27,66 +30,36 @@ 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 +class ContentViewAddResourcesView(NewAddRemoveResourcesView): + remove_button = Dropdown(locator='.//div[@data-ouia-component-id="cv-components-bulk-actions"]') + add_button = Button(locator='.//button[@data-ouia-component-id="add-content-views"]') table = PatternflyTable( - component_id='OUIA-Generated-Table-4', + component_id='content-view-components-table', column_widgets={ 0: Checkbox(locator='.//input[@type="checkbox"]'), 'Type': Text('.//a'), 'Name': Text('.//a'), - 'Product': Text('.//a'), - 'Sync State': Text('.//a'), - 'Content': Text('.//a'), + 'Version': Text('.//a'), + 'Environments': Text('.//td[5]'), + 'Repositories': Text('.//a'), 'Status': Text('.//a'), + 'Description': Text('.//a'), + 8: Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]'), }, ) - 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 AddContentViewModal(BaseLoggedInView): + title = Text('.//div[@data-ouia-component-id="add-content-views"]') + submit_button = PF4Button('add-components-modal-add') + cancel_button = PF4Button('add-components-modal-cancel') + + version_select = Select(locator=".//div[@data-ouia-component-id='add-content-views']") + always_update = Checkbox(locator=".//input[@class='pf-c-check__input']") + + @property + def is_displayed(self): + return self.title.is_displayed class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): @@ -95,7 +68,9 @@ class ContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): table = ExpandableTable( component_id='content-views-table', column_widgets={ + 'Type': Text('./a'), 'Name': Text('./a'), + 'Last Published': ('./a'), 'Last task': Text('.//a'), 'Latest version': Text('.//a'), }, @@ -114,22 +89,11 @@ class ContentViewCreateView(BaseLoggedInView): submit = PF4Button('create-content-view-form-submit') cancel = PF4Button('create-content-view-form-cancel') - @View.nested - class component(View): - component_tile = Text('//div[contains(@id, "component")]') - solve_dependencies = Checkbox(id='dependencies') - import_only = Checkbox(id='importOnly') - - def child_widget_accessed(self, widget): - self.component_tile.click() - - @View.nested - class composite(View): - composite_tile = Text('//div[contains(@id, "composite")]') - auto_publish = Checkbox(id='autoPublish') - - def child_widget_accessed(self, widget): - self.composite_tile.click() + component_tile = Text('//div[contains(@id, "component")]') + solve_dependencies = Checkbox(id='dependencies') + import_only = Checkbox(id='importOnly') + composite_tile = Text('//div[contains(@id, "composite")]') + auto_publish = Checkbox(id='autoPublish') @property def is_displayed(self): @@ -174,7 +138,7 @@ class versions(Tab): column_widgets={ 0: Checkbox(locator='.//input[@type="checkbox"]'), 'Version': Text('.//a'), - 'Environments': Text('.//a'), + 'Environments': Text('.//td[3]'), 'Packages': Text('.//a'), 'Errata': Text('.//a'), 'Additional content': Text('.//a'), @@ -200,7 +164,7 @@ def search(self, version_name): class content_views(Tab): TAB_LOCATOR = ParametrizedLocator('//a[contains(@href, "#/contentviews")]') - resources = View.nested(NewAddRemoveResourcesView) + resources = View.nested(ContentViewAddResourcesView) @View.nested class repositories(Tab): @@ -239,14 +203,13 @@ class ContentViewVersionPublishView(BaseLoggedInView): 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') + next_button = Button('Next') + finish_button = Button('Finish') + back_button = Button('Back') + cancel_button = Button('Cancel') close_button = Button('Close') progressbar = PF4ProgressBar('.//div[contains(@class, "pf-c-wizard__main-body")]') + lce_selector = ParametrizedView.nested(PF4LCECheckSelectorGroup) @property def is_displayed(self): @@ -273,6 +236,15 @@ def before_fill(self, values=None): ) +class ContentViewVersionPromoteView(Modal): + ROOT = './/div[@data-ouia-component-id="promote-version"]' + + description = Text('.//h2[@data-ouia-component-id="description-text-value"]') + lce_selector = ParametrizedView.nested(PF4LCESelectorGroup) + promote_btn = Button(locator='//button[normalize-space(.)="Promote"]') + cancel_btn = Button(locator='//button[normalize-space(.)="Cancel"]') + + class ContentViewVersionDetailsView(BaseLoggedInView): breadcrumb = BreadCrumb() version = Text(locator='.//h2[@data-ouia-component-id="cv-version"]') diff --git a/airgun/widgets.py b/airgun/widgets.py index ff5a35d3d..4873afd25 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -1510,6 +1510,13 @@ def checkbox_selected(self, locator): return self.browser.is_selected(locator) +class PF4LCECheckSelector(PF4LCESelector): + """Checkbox version of PF4 LCE Selector""" + + LABELS = './/label[contains(@class, "pf-c-check__label")]' + CHECKBOX = './/input[contains(@class, "pf-c-check") and ../label[.//*[contains(text(), "{}")]]]' + + class LimitInput(Widget): """Input for managing limits (e.g. Hosts limit). Consists of 'Unlimited' checkbox and text input for specifying the limit, which is only visible if