From f43c0e230cb4f1c45d8e0f40f72caa0b1e44c153 Mon Sep 17 00:00:00 2001 From: Peter Dragun Date: Tue, 2 Aug 2022 11:12:11 +0200 Subject: [PATCH 01/35] fix tasks pagination --- airgun/entities/task.py | 5 +++++ airgun/views/task.py | 8 ++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/airgun/entities/task.py b/airgun/entities/task.py index ebbef4068..bf0c84786 100644 --- a/airgun/entities/task.py +++ b/airgun/entities/task.py @@ -40,6 +40,11 @@ def set_chart_filter(self, chart_name, index=None): else: chart.name.click() + def total_items(self): + """Get total items displayed in the table""" + view = self.navigate_to(self, 'All') + return view.pagination.total_items + @navigator.register(TaskEntity, 'All') class ShowAllTasks(NavigateStep): diff --git a/airgun/views/task.py b/airgun/views/task.py index 07eaff3da..3b09284b5 100644 --- a/airgun/views/task.py +++ b/airgun/views/task.py @@ -3,12 +3,12 @@ from widgetastic.widget import Text from widgetastic.widget import View from widgetastic_patternfly import BreadCrumb +from widgetastic_patternfly4 import Pagination from airgun.views.common import BaseLoggedInView from airgun.views.common import SatTab from airgun.views.common import SearchableViewMixin from airgun.widgets import ActionsDropdown -from airgun.widgets import Pagination from airgun.widgets import PieChart from airgun.widgets import ProgressBar from airgun.widgets import ReadOnlyEntry @@ -26,10 +26,6 @@ class TaskReadOnlyEntryError(ReadOnlyEntry): BASE_LOCATOR = "//span[contains(., '{}')]//parent::div" "/following-sibling::pre" -class TaskPagination(Pagination): - PER_PAGE_BUTTON_DROPDOWN = ".//div[button[@id='tasks-table-dropdown']]" - - class TasksView(BaseLoggedInView, SearchableViewMixin): title = Text("//h1[normalize-space(.)='Tasks']") focus = ActionsDropdown("//div[./button[@id='tasks-dashboard-time-period-dropdown']]") @@ -39,7 +35,7 @@ class TasksView(BaseLoggedInView, SearchableViewMixin): 'Action': Text('./a'), }, ) - pagination = TaskPagination() + pagination = Pagination() @property def is_displayed(self): From 96c288aee002b6b693cd65e0ab1c821ab51f3cb9 Mon Sep 17 00:00:00 2001 From: jyejare Date: Tue, 2 Aug 2022 23:39:24 +0530 Subject: [PATCH 02/35] AutoCherrypick GHAs --- .github/workflows/auto_cherry_pick.yml | 43 ++++++++++++++++++++++++++ .github/workflows/required_labels.yml | 17 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .github/workflows/auto_cherry_pick.yml create mode 100644 .github/workflows/required_labels.yml diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml new file mode 100644 index 000000000..a5de439ce --- /dev/null +++ b/.github/workflows/auto_cherry_pick.yml @@ -0,0 +1,43 @@ +# CI stages to execute against all branches on PR merge +name: auto_cherry_pick_commits + +on: + pull_request_target: + types: + - closed + +jobs: + branch-matrix: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') + name: Generate a branch matrix to apply cherrypicks + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.set-matrix.outputs.branches }} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - id: set-matrix + run: echo "::set-output name=branches::$(git branch -rl --sort=-authordate 'origin/6.*.z' --format='%(refname:lstrip=-1)' | head -n2 | jq -cnR '[inputs | select(length>0)]')" + auto_cherry_picking: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') + name: Auto Cherry picking + needs: branch-matrix + runs-on: ubuntu-latest + strategy: + matrix: + to_branch: ${{ fromJson(needs.branch-matrix.outputs.branches) }} + steps: + - name: Checkout Airgun + uses: actions/checkout@v3 + with: + fetch-depth: 0 + if: matrix.to_branch != github.base_ref + - name: Cherry pick into ${{ matrix.to_branch }} + uses: carloscastrojumo/github-cherry-pick-action@v1.0.2 + with: + branch: ${{ matrix.to_branch }} + labels: | + Auto_Cherry_Picked + # skipping PRs remote target_branch from cherrypicking into itself + if: matrix.to_branch != github.base_ref diff --git a/.github/workflows/required_labels.yml b/.github/workflows/required_labels.yml new file mode 100644 index 000000000..3d66f77f8 --- /dev/null +++ b/.github/workflows/required_labels.yml @@ -0,0 +1,17 @@ +# CI jobs to check specific labels present / absent +name: required_labels + +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] + +jobs: + cherrypick_label: + name: Enforcing cherrypick labels + runs-on: ubuntu-latest + steps: + - uses: mheap/github-action-required-labels@v2 + with: + mode: exactly + count: 1 + labels: "CherryPick, No-CherryPick" From d410b75e455e11d4201410f20dfb7a7c3b5f2b91 Mon Sep 17 00:00:00 2001 From: Griffin Sullivan Date: Thu, 14 Jul 2022 12:15:57 -0400 Subject: [PATCH 03/35] Fixing PF4ConfirmationDialog --- airgun/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/airgun/widgets.py b/airgun/widgets.py index 7468eab33..8ac014623 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -1134,9 +1134,9 @@ class Pf4ConfirmationDialog(ConfirmationDialog): right corner.""" ROOT = '//div[@id="app-confirm-modal" or @data-ouia-component-type="PF4/ModalContent"]' - confirm_dialog = PF4Button('OUIA-Generated-Button-danger-1') - cancel_dialog = PF4Button('OUIA-Generated-Button-link-1') - discard_dialog = PF4Button('OUIA-Generated-Button-plain-1') + confirm_dialog = PF4Button('btn-modal-confirm') + cancel_dialog = PF4Button('btn-modal-cancel') + discard_dialog = PF4Button('OUIA-Generated-Modal-small-1-ModalBoxCloseButton') class LCESelector(GenericLocatorWidget): From 6b60ed2c42c56cbec02eb4bb095c54dac9bdd297 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Wed, 17 Aug 2022 11:27:56 +0200 Subject: [PATCH 04/35] Fix locator for column_value in PopOverWidget (#736) --- airgun/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/widgets.py b/airgun/widgets.py index 8ac014623..9ec926171 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -2247,7 +2247,7 @@ class PopOverWidget(View): """ ROOT = '.' - column_value = Text(".//div[contains(@class,'ellipsis editable')]") + column_value = Text(".//div[contains(@class,'ellipsis-pf-tooltip editable')]") pop_over_view = PopOverModalView() def fill(self, item): From 79ef1ff2e9c6fce10faed1b6603a66fbc18fe34a Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Wed, 17 Aug 2022 11:31:47 +0200 Subject: [PATCH 05/35] use new host UI by default (#735) --- airgun/entities/host.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/airgun/entities/host.py b/airgun/entities/host.py index 44dde66c3..c8e2d5a6a 100644 --- a/airgun/entities/host.py +++ b/airgun/entities/host.py @@ -26,6 +26,7 @@ from airgun.views.host import HostsUnassignCompliancePolicy from airgun.views.host import HostsView from airgun.views.host import RecommendationListView +from airgun.views.host_new import NewHostDetailsView class HostEntity(BaseEntity): @@ -39,7 +40,7 @@ def create(self, values): view.fill(values) self.browser.click(view.submit, ignore_ajax=True) self.browser.plugin.ensure_page_safe(timeout='600s') - host_view = HostDetailsView(self.browser) + host_view = NewHostDetailsView(self.browser) host_view.wait_displayed() host_view.flash.assert_no_error() host_view.flash.dismiss() @@ -327,6 +328,10 @@ 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() + host_view = NewHostDetailsView(self.parent.browser) + host_view.wait_displayed() + host_view.dropdown.wait_displayed() + host_view.dropdown.item_select('Legacy UI') @navigator.register(HostEntity, 'Edit') From 09cd65b0d0d6781ed567d7d9f42075959645c911 Mon Sep 17 00:00:00 2001 From: Adarsh dubey Date: Fri, 19 Aug 2022 15:22:25 +0530 Subject: [PATCH 06/35] Fixing locator for login page footer (#738) --- airgun/views/login.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/airgun/views/login.py b/airgun/views/login.py index a5bc341ff..1422f6780 100644 --- a/airgun/views/login.py +++ b/airgun/views/login.py @@ -7,10 +7,9 @@ class LoginView(View, ClickableMixin): username = TextInput(id="login_login") password = TextInput(id="login_password") - login_text = Text('//*[@id="login-footer-text"]') + login_text = Text(".//footer[contains(@class,'login-pf-page-footer')]") logo = Text('//img[@alt="logo"]') submit = Text('//button[@type="submit"]') - version = Text('//*[@id="version"]') @property def is_displayed(self): From 704cfbc635dd54801a8bcb86f7ebf73a60172a1f Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Tue, 23 Aug 2022 16:29:41 +0200 Subject: [PATCH 07/35] Add content tab to new host UI (#720) * add content view * add all filers and locators from 6.12 * add repo sets tab * add install packages view * add repository sets view * add entities to new host UI content tab * ensure details page displayed * fix module stream dropdown * fix errata apply dropdown * use lowercase card names * update status locators * add parent context to host details card * add BZ link --- airgun/entities/host_new.py | 83 ++++++++++++++ airgun/views/host_new.py | 217 ++++++++++++++++++++++++++++++------ 2 files changed, 269 insertions(+), 31 deletions(-) diff --git a/airgun/entities/host_new.py b/airgun/entities/host_new.py index 1dd13cfd3..250c18f94 100644 --- a/airgun/entities/host_new.py +++ b/airgun/entities/host_new.py @@ -1,6 +1,8 @@ from airgun.entities.host import HostEntity from airgun.navigation import NavigateStep from airgun.navigation import navigator +from airgun.views.host_new import InstallPackagesView +from airgun.views.host_new import ModuleStreamDialog from airgun.views.host_new import NewHostDetailsView @@ -23,6 +25,87 @@ def get_details(self, entity_name, widget_names=None): view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) return view.read(widget_names=widget_names) + def get_packages(self, entity_name, search=""): + """Filter installed packages on host""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.content.packages.select() + view.content.packages.searchbar.fill(search) + # wait for filter to apply + self.browser.plugin.ensure_page_safe() + return view.content.packages.read() + + def install_package(self, entity_name, package): + """Installs package on host using the installation modal""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.packages.select() + view.content.packages.dropdown.wait_displayed() + view.content.packages.dropdown.item_select('Install packages') + view = InstallPackagesView(self.browser) + view.wait_displayed() + view.searchbar.fill(package) + # wait for filter to apply + self.browser.plugin.ensure_page_safe() + view.select_all.click() + view.install.click() + + def apply_package_action(self, entity_name, package_name, action): + """Apply `action` to selected package based on the `package_name`""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.packages.searchbar.fill(package_name) + # wait for filter to apply + self.browser.plugin.ensure_page_safe() + view.content.packages.table.wait_displayed() + view.content.packages.table[0][5].widget.item_select(action) + view.flash.assert_no_error() + view.flash.dismiss() + + def get_errata_by_type(self, entity_name, type): + """List errata based on type and return table""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.errata.select() + view.content.errata.type_filter.fill(type) + return view.read(widget_names="content.errata.table") + + def apply_erratas(self, entity_name, search): + """Apply errata on selected host based on errata_id""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.errata.searchbar.fill(search) + # wait for filter to apply + self.browser.plugin.ensure_page_safe() + view.content.errata.select_all.click() + view.content.errata.apply.click() + view.flash.assert_no_error() + view.flash.dismiss() + + def get_module_streams(self, entity_name, search): + """Filter module streams""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.module_streams.select() + view.content.module_streams.searchbar.fill(search) + # wait for filter to apply + self.browser.plugin.ensure_page_safe() + view.content.module_streams.table.wait_displayed() + return view.content.module_streams.table.read() + + def apply_module_streams_action(self, entity_name, module_stream, action): + """Apply `action` to selected Module stream based on the `module_stream`""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.module_streams.searchbar.fill(module_stream) + # wait for filter to apply + self.browser.plugin.ensure_page_safe() + view.content.module_streams.table[0][5].widget.item_select(action) + modal = ModuleStreamDialog(self.browser) + if modal.is_displayed: + modal.confirm() + view.flash.assert_no_error() + view.flash.dismiss() + @navigator.register(NewHostEntity, 'NewDetails') class ShowNewHostDetails(NavigateStep): diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index 0473b0f6b..609aa3be5 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -1,12 +1,21 @@ +from widgetastic.widget import Checkbox from widgetastic.widget import Text +from widgetastic.widget import TextInput from widgetastic.widget import View from widgetastic.widget import Widget +from widgetastic.widget.table import Table +from widgetastic_patternfly4 import Button from widgetastic_patternfly4 import Dropdown +from widgetastic_patternfly4 import Pagination +from widgetastic_patternfly4 import Select from widgetastic_patternfly4 import Tab from widgetastic_patternfly4.ouia import BreadCrumb -from widgetastic_patternfly4.ouia import Button +from widgetastic_patternfly4.ouia import Button as OUIAButton +from widgetastic_patternfly4.ouia import ExpandableTable +from widgetastic_patternfly4.ouia import PatternflyTable from airgun.views.common import BaseLoggedInView +from airgun.widgets import Pf4ConfirmationDialog class Card(View): @@ -15,21 +24,25 @@ class Card(View): title = Text('.//div[@class="pf-c-card__title"]') +class DropdownWithDescripton(Dropdown): + """Dropdown with description below items""" + + ITEM_LOCATOR = ".//*[contains(@class, 'pf-c-dropdown__menu-item') and contains(text(), {})]" + + class HostDetailsCard(Widget): """Details card body contains multiple host detail information""" - LABELS = './/div[@class="pf-c-description-list__group"]//dt//span' - VALUES = ( - './/div[@class="pf-c-description-list__group"]//dd//descendant::*/normalize-space(.)/..' - ) + LABELS = '//div[@class="pf-c-description-list__group"]//dt//span' + VALUES = '//div[@class="pf-c-description-list__group"]//dd//descendant::*/text()/..' def read(self): """Return a dictionary where keys are property names and values are property values. Values are either in span elements or in div elements """ items = {} - labels = self.browser.elements(self.LABELS) - values = self.browser.elements(self.VALUES) + labels = self.browser.elements(f'{self.parent.ROOT}{self.LABELS}') + values = self.browser.elements(f'{self.parent.ROOT}{self.VALUES}') # the length of elements should be always same if len(values) != len(labels): raise AttributeError( @@ -49,54 +62,196 @@ class NewHostDetailsView(BaseLoggedInView): @property def is_displayed(self): - breadcrumb_loaded = self.browser.wait_for_element( - self.Overview.RecentJobsCard.is_table_loaded, exception=False - ) + breadcrumb_loaded = self.browser.wait_for_element(self.breadcrumb, exception=False) return breadcrumb_loaded and self.breadcrumb.locations[0] == 'Hosts' - edit = Button('OUIA-Generated-Button-secondary-1') + edit = OUIAButton('OUIA-Generated-Button-secondary-1') dropdown = Dropdown(locator='//button[@id="hostdetails-kebab"]/..') @View.nested - class Overview(Tab): + class overview(Tab): ROOT = './/div[contains(@class, "host-details-tab-item")]' @View.nested - class DetailsCard(Card): - ROOT = '(.//article[contains(@class, "pf-c-card")])[1]' + class details(Card): + ROOT = './/article[.//div[text()="Details"]]' details = HostDetailsCard() @View.nested - class HostStatusCard(Card): - ROOT = '(//article[contains(@class, "pf-c-card")])[2]' + class host_status(Card): + ROOT = './/article[.//span[text()="Host status"]]' status = Text('.//h4[contains(@data-ouia-component-id, "OUIA-Generated-Title")]') - status_success = Text('.//span[@class="status-success"]') - status_warning = Text('.//span[@class="status-warning"]') - status_error = Text('.//span[@class="status-error"]') - status_disabled = Text('.//span[@class="disabled"]') + status_success = Text('.//a[span[@class="status-success"]]') + status_warning = Text('.//a[span[@class="status-warning"]]') + status_error = Text('.//a[span[@class="status-error"]]') + status_disabled = Text('.//a[span[@class="disabled"]]') @View.nested - class RecentJobsCard(Card): - ROOT = '(//article[contains(@class, "pf-c-card")])[5]' + class installable_errata(Card): + ROOT = './/article[.//div[text()="Installable errata"]]' + + security_advisory = Text('.//a[contains(@href, "type=security")]') + bug_fixes = Text('.//a[contains(@href, "type=bugfix")]') + enhancements = Text('.//a[contains(@href, "type=enhancement")]') + + @View.nested + class total_risks(Card): + ROOT = './/article[.//div[text()="Total risks"]]' + + low = Text('.//*[@id="legend-labels-0"]/*') + moderate = Text('.//*[@id="legend-labels-1"]/*') + important = Text('.//*[@id="legend-labels-2"]/*') + critical = Text('.//*[@id="legend-labels-3"]/*') + + @View.nested + class recent_jobs(Card): + ROOT = './/article[.//div[text()="Recent jobs"]]' is_table_loaded = './/ul[@aria-label="recent-jobs-table"]' @View.nested - class Content(Tab): - pass + class content(Tab): + # TODO Setting ROOT is just a workaround because of BZ 2119076, + # once this gets fixed we should use the parametrized locator from Tab class + ROOT = './/div' - @View.nested - class Traces(Tab): - enable_traces = Button('OUIA-Generated-Button-primary-1') + @View.nested + class packages(Tab): + # workaround for BZ 2119076 + ROOT = './/div[@id="packages-tab"]' + + select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') + searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + status_filter = Dropdown(locator='.//div[@aria-label="select Status container"]/div') + upgrade = Button(locator='.//button[normalize-space(.)="Upgrade"]') + dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') + + table = PatternflyTable( + component_id="host-packages-table", + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'Package': Text('./parent::td'), + 'Status': Text('./span'), + 'Installed version': Text('./parent::td'), + 'Upgradable to': Text('./span'), + 5: Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]'), + }, + ) + pagination = Pagination() + + @View.nested + class errata(Tab): + # workaround for BZ 2119076 + ROOT = './/div[@id="errata-tab"]' + + select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') + searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + type_filter = Select(locator='.//div[@aria-label="select Type container"]/div') + severity_filter = Select(locator='.//div[@aria-label="select Severity container"]/div') + apply = Button(locator='.//button[normalize-space(.)="Apply"]') + dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') + + table = ExpandableTable( + component_id="host-errata-table", + column_widgets={ + 1: Checkbox(locator='.//input[@type="checkbox"]'), + 'Errata': Text('./a'), + 'Type': Text('./span'), + 'Severity': Text('./span'), + 'Installable': Text('./span'), + 'Synopsis': Text('./span'), + 'Published date': Text('./span/span'), + 8: Dropdown(locator='./div'), + }, + ) + pagination = Pagination() + + @View.nested + class module_streams(Tab): + TAB_NAME = 'Module streams' + # workaround for BZ 2119076 + ROOT = './/div[@id="modulestreams-tab"]' + + searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + status_filter = Select(locator='.//div[@aria-label="select Status container"]/div') + installation_status_filter = Select( + locator='.//div[@aria-label="select Installation status container"]/div' + ) + dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') + + table = Table( + locator='.//table[@aria-label="Content View Table"]', + column_widgets={ + 'Name': Text('./a'), + 'State': Text('.//span'), + 'Stream': Text('./parent::td'), + 'Installation status': Text('.//small'), + 'Installed profile': Text('./parent::td'), + 5: DropdownWithDescripton(locator='.//div[contains(@class, "pf-c-dropdown")]'), + }, + ) + pagination = Pagination() + + @View.nested + class repository_sets(Tab): + TAB_NAME = 'Repository sets' + # workaround for BZ 2119076 + ROOT = './/div[@id="repo-sets-tab"]' + + select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') + searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + status_filter = Select(locator='.//div[@aria-label="select Status container"]/div') + dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') + + table = Table( + locator='.//table[@aria-label="Content View Table"]', + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'Repository': Text('./span'), + 'Product': Text('./a'), + 'Repository path': Text('./span'), + 'Status': Text('.//span[contains(@class, "pf-c-label__content")]'), + 5: Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]'), + }, + ) + pagination = Pagination() @View.nested - class RepositorySets(Tab): - pass + class traces(Tab): + enable_traces = OUIAButton('OUIA-Generated-Button-primary-1') @View.nested - class Ansible(Tab): + class ansible(Tab): pass @View.nested - class Insights(Tab): + class insights(Tab): pass + + +class InstallPackagesView(View): + """Install packages modal""" + + ROOT = './/div[@id="package-install-modal"]' + + select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') + searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + + table = Table( + locator='.//table[@aria-label="Content View Table"]', + column_widgets={ + 0: Checkbox(locator='.//input[@type="checkbox"]'), + 'Package': Text('./parent::td'), + 'Version': Text('./parent::td'), + }, + ) + pagination = Pagination() + + install = Button(locator='.//button[(normalize-space(.)="Install")]') + cancel = Button('Cancel') + + +class ModuleStreamDialog(Pf4ConfirmationDialog): + + confirm_dialog = Button(locator='.//button[@aria-label="confirm-module-action"]') + cancel_dialog = Button(locator='.//button[@aria-label="cancel-module-action"]') From e26931eaede23e34f060b2bc2585069829b9cb59 Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Mon, 5 Sep 2022 11:16:04 +0200 Subject: [PATCH 08/35] add rex related views and entities (#742) --- airgun/entities/host_new.py | 15 +++++++++++- airgun/views/host_new.py | 47 +++++++++++++++++++++++++++++++++++-- airgun/widgets.py | 45 +++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/airgun/entities/host_new.py b/airgun/entities/host_new.py index 250c18f94..c2652a455 100644 --- a/airgun/entities/host_new.py +++ b/airgun/entities/host_new.py @@ -4,6 +4,7 @@ from airgun.views.host_new import InstallPackagesView from airgun.views.host_new import ModuleStreamDialog from airgun.views.host_new import NewHostDetailsView +from airgun.views.job_invocation import JobInvocationCreateView class NewHostEntity(HostEntity): @@ -23,8 +24,20 @@ def get_details(self, entity_name, widget_names=None): will be read. """ view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + self.browser.plugin.ensure_page_safe() return view.read(widget_names=widget_names) + def schedule_job(self, entity_name, values): + """Schedule a remote execution on selected host""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.schedule_job.fill('Schedule a job') + view = JobInvocationCreateView(self.browser) + self.browser.plugin.ensure_page_safe() + view.wait_displayed() + view.fill(values) + view.submit.click() + def get_packages(self, entity_name, search=""): """Filter installed packages on host""" view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) @@ -77,7 +90,7 @@ def apply_erratas(self, entity_name, search): # wait for filter to apply self.browser.plugin.ensure_page_safe() view.content.errata.select_all.click() - view.content.errata.apply.click() + view.content.errata.apply.fill('Apply') view.flash.assert_no_error() view.flash.dismiss() diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index 609aa3be5..afeecfe5d 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -15,6 +15,7 @@ from widgetastic_patternfly4.ouia import PatternflyTable from airgun.views.common import BaseLoggedInView +from airgun.widgets import Pf4ActionsDropdown from airgun.widgets import Pf4ConfirmationDialog @@ -30,6 +31,29 @@ class DropdownWithDescripton(Dropdown): ITEM_LOCATOR = ".//*[contains(@class, 'pf-c-dropdown__menu-item') and contains(text(), {})]" +class ItemList(Widget): + """Item list view, similar to table view, but using the ul/li structure""" + + ROW = '//li/div[@class="pf-c-data-list__item-row"]/div' + COLUMN = './div' + + def read(self): + """Return content of table""" + items = [] + # make sure that parent locator works for str and also Locator object + parent_locator = ( + self.parent.ROOT if type(self.parent.ROOT) == str else f'({self.parent.ROOT.locator})' + ) + rows = self.browser.elements(f'{parent_locator}{self.ROW}') + for row in rows: + columns = [] + elements = row.find_elements("xpath", self.COLUMN) + for element in elements: + columns.append(element.text) + items.append(columns) + return items + + class HostDetailsCard(Widget): """Details card body contains multiple host detail information""" @@ -67,6 +91,7 @@ def is_displayed(self): edit = OUIAButton('OUIA-Generated-Button-secondary-1') dropdown = Dropdown(locator='//button[@id="hostdetails-kebab"]/..') + schedule_job = Pf4ActionsDropdown(locator='.//div[div/button[@aria-label="Select"]]') @View.nested class overview(Tab): @@ -87,6 +112,12 @@ class host_status(Card): status_error = Text('.//a[span[@class="status-error"]]') status_disabled = Text('.//a[span[@class="disabled"]]') + class recent_audits(Card): + ROOT = './/article[.//div[text()="Recent audits"]]' + + all_audits = Text('.//a[normalize-space(.)="All audits"]') + table = ItemList() + @View.nested class installable_errata(Card): ROOT = './/article[.//div[text()="Installable errata"]]' @@ -108,6 +139,16 @@ class total_risks(Card): class recent_jobs(Card): ROOT = './/article[.//div[text()="Recent jobs"]]' is_table_loaded = './/ul[@aria-label="recent-jobs-table"]' + actions = Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]') + + class finished(Tab): + table = ItemList() + + class running(Tab): + table = ItemList() + + class scheduled(Tab): + table = ItemList() @View.nested class content(Tab): @@ -123,7 +164,9 @@ class packages(Tab): select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') status_filter = Dropdown(locator='.//div[@aria-label="select Status container"]/div') - upgrade = Button(locator='.//button[normalize-space(.)="Upgrade"]') + upgrade = Pf4ActionsDropdown( + locator='.//div[div/button[normalize-space(.)="Upgrade"]]' + ) dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') table = PatternflyTable( @@ -148,7 +191,7 @@ class errata(Tab): searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') type_filter = Select(locator='.//div[@aria-label="select Type container"]/div') severity_filter = Select(locator='.//div[@aria-label="select Severity container"]/div') - apply = Button(locator='.//button[normalize-space(.)="Apply"]') + apply = Pf4ActionsDropdown(locator='.//div[@aria-label="errata_dropdown"]') dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') table = ExpandableTable( diff --git a/airgun/widgets.py b/airgun/widgets.py index 9ec926171..3bbc0e562 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -633,6 +633,51 @@ def read(self): return self.items +class Pf4ActionsDropdown(ActionsDropdown): + """PF4 version of actions dropdown with support for items description + + Example html representation:: + + + + """ + + button = Text( + './/button[contains(@class,"pf-c-dropdown__toggle-button")' + 'and not(@data-ouia-component-type="PF4/DropdownToggle")]' + ) + dropdown = Text( + './/button[contains(@class,"pf-c-dropdown__toggle-button")' + 'and @data-ouia-component-type="PF4/DropdownToggle"]' + ) + ITEMS_LOCATOR = ".//ul[contains(@class, 'pf-c-dropdown__menu')]/li" + ITEM_LOCATOR = ".//ul/li[@role='menuitem' and contains(normalize-space(.), '{}')]" + + @property + def is_open(self): + return 'pf-m-expanded' in self.browser.classes(self) + + @property + def is_enabled(self): + return 'pf-m-disabled' not in self.browser.classes(self) + + def select(self, item): + self.open() + self.browser.element(self.ITEM_LOCATOR.format(item), parent=self).click() + + class ActionDropdownWithCheckbox(ActionsDropdown): """Custom drop down which contains the checkbox inside in drop down.""" From b28dfd4dd7780a155a91e36868acf2ab66eb9dd0 Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Tue, 6 Sep 2022 14:12:40 +0200 Subject: [PATCH 09/35] replace find_element_by_xpath with find_element (#744) --- airgun/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/widgets.py b/airgun/widgets.py index 3bbc0e562..40307c3a7 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -2392,7 +2392,7 @@ def item_select(self, items, close=True): try: for item in items: element = self.item_element(item, close=False) - if not element.find_element_by_xpath("./..").get_attribute('aria-selected'): + if not element.find_element("xpath", "./..").get_attribute('aria-selected'): element.click() finally: self.browser.click(self.BUTTON_LOCATOR) From 1a3e0c30f402707de74689e7dd9153cc26658405 Mon Sep 17 00:00:00 2001 From: Jameer Pathan Date: Tue, 6 Sep 2022 14:12:55 +0200 Subject: [PATCH 10/35] Add support for container repo discovery (#745) --- airgun/views/product.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/airgun/views/product.py b/airgun/views/product.py index ac6b3da6e..fe03ddcd0 100644 --- a/airgun/views/product.py +++ b/airgun/views/product.py @@ -136,6 +136,10 @@ class ProductRepoDiscoveryView(BaseLoggedInView, SearchableViewMixin): breadcrumb = BreadCrumb() repo_type = Select(locator="//select[@ng-model='discovery.contentType']") url = TextInput(id='urlToDiscover') + registry_type = Select(id='registry_type') + username = TextInput(id='upstreamUsername') + password = TextInput(id='upstreamPassword') + registry_search = TextInput(id='registrySearch') @property def is_displayed(self): From c7406356548bcee94bf49f1542353df37fd9a9f0 Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Mon, 12 Sep 2022 20:02:59 +0200 Subject: [PATCH 11/35] Ensure cockpit page is safe before locating elements (#743) * ensure cockpit page is safe before locating elements * find_elements fix --- airgun/entities/host.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/airgun/entities/host.py b/airgun/entities/host.py index c8e2d5a6a..132e7e706 100644 --- a/airgun/entities/host.py +++ b/airgun/entities/host.py @@ -259,6 +259,7 @@ def get_webconsole_content(self, entity_name, rhel_version=7): # switch to the last opened tab, self.browser.switch_to_window(self.browser.window_handles[-1]) + self.browser.plugin.ensure_page_safe() self.browser.wait_for_element(locator='//div[@id="content"]/iframe', exception=True) # the remote host content is loaded in an iframe, let's switch to it self.browser.switch_to_frame(locator='//div[@id="content"]/iframe') @@ -266,8 +267,8 @@ def get_webconsole_content(self, entity_name, rhel_version=7): self.browser.wait_for_element( locator=f'//{hostname_element}[@id="{hostname_id}"]', exception=True, visible=True ) - hostname_button_view = self.browser.selenium.find_elements_by_id(hostname_id) - hostname = hostname_button_view[0].text + hostname_button = self.browser.selenium.find_elements("id", hostname_id) + hostname = hostname_button[0].text self.browser.switch_to_main_frame() self.browser.switch_to_window(self.browser.window_handles[0]) self.browser.close_window(self.browser.window_handles[-1]) From 9c236168182467afd1a5c0d52b9acfd2d25efc78 Mon Sep 17 00:00:00 2001 From: omkarkhatavkar Date: Thu, 8 Sep 2022 18:39:16 +0530 Subject: [PATCH 12/35] updating the airgun autocherrypick n-1 version as robottelo --- .github/workflows/auto_cherry_pick.yml | 27 ++++++++++++-------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index a5de439ce..b173f675c 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -7,37 +7,34 @@ on: - closed jobs: - branch-matrix: + previous-branch: if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') - name: Generate a branch matrix to apply cherrypicks + name: Calculate previous branch name runs-on: ubuntu-latest outputs: - branches: ${{ steps.set-matrix.outputs.branches }} + previous_branch: ${{ steps.set-branch.outputs.previous_branch }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - - id: set-matrix - run: echo "::set-output name=branches::$(git branch -rl --sort=-authordate 'origin/6.*.z' --format='%(refname:lstrip=-1)' | head -n2 | jq -cnR '[inputs | select(length>0)]')" - auto_cherry_picking: + - id: set-branch + run: echo "::set-output name=previous_branch::$(if [ $GITHUB_BASE_REF == 'master' ]; then echo $(git branch -rl 'origin/6.*.z' --format='%(refname:lstrip=-1)' | sort --version-sort | tail -n1 | jq -cnR '[inputs | select(length>0)]'); else echo ['"6.'$(($(echo $GITHUB_BASE_REF | cut -d. -f2) - 1))'.z"']; fi)" + + auto-cherry-pick: + name: Auto Cherry Pick to previous branch if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') - name: Auto Cherry picking - needs: branch-matrix + needs: previous-branch runs-on: ubuntu-latest strategy: matrix: - to_branch: ${{ fromJson(needs.branch-matrix.outputs.branches) }} + to_branch: ${{ fromJson(needs.previous-branch.outputs.previous_branch) }} steps: - - name: Checkout Airgun - uses: actions/checkout@v3 + - uses: actions/checkout@v3 with: fetch-depth: 0 - if: matrix.to_branch != github.base_ref - name: Cherry pick into ${{ matrix.to_branch }} - uses: carloscastrojumo/github-cherry-pick-action@v1.0.2 + uses: jyejare/github-cherry-pick-action@main with: branch: ${{ matrix.to_branch }} labels: | Auto_Cherry_Picked - # skipping PRs remote target_branch from cherrypicking into itself - if: matrix.to_branch != github.base_ref From f1c54d8f22d7e4f577e65444a64c774bb61026a0 Mon Sep 17 00:00:00 2001 From: Peter Dragun Date: Thu, 15 Sep 2022 14:00:16 +0200 Subject: [PATCH 13/35] recent audits and jobs as table --- airgun/views/host_new.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index afeecfe5d..9013ef29c 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -17,6 +17,7 @@ from airgun.views.common import BaseLoggedInView from airgun.widgets import Pf4ActionsDropdown from airgun.widgets import Pf4ConfirmationDialog +from airgun.widgets import SatTableWithoutHeaders class Card(View): @@ -31,29 +32,6 @@ class DropdownWithDescripton(Dropdown): ITEM_LOCATOR = ".//*[contains(@class, 'pf-c-dropdown__menu-item') and contains(text(), {})]" -class ItemList(Widget): - """Item list view, similar to table view, but using the ul/li structure""" - - ROW = '//li/div[@class="pf-c-data-list__item-row"]/div' - COLUMN = './div' - - def read(self): - """Return content of table""" - items = [] - # make sure that parent locator works for str and also Locator object - parent_locator = ( - self.parent.ROOT if type(self.parent.ROOT) == str else f'({self.parent.ROOT.locator})' - ) - rows = self.browser.elements(f'{parent_locator}{self.ROW}') - for row in rows: - columns = [] - elements = row.find_elements("xpath", self.COLUMN) - for element in elements: - columns.append(element.text) - items.append(columns) - return items - - class HostDetailsCard(Widget): """Details card body contains multiple host detail information""" @@ -116,7 +94,7 @@ class recent_audits(Card): ROOT = './/article[.//div[text()="Recent audits"]]' all_audits = Text('.//a[normalize-space(.)="All audits"]') - table = ItemList() + table = SatTableWithoutHeaders(locator='.//table[@aria-label="audits table"]') @View.nested class installable_errata(Card): @@ -138,17 +116,16 @@ class total_risks(Card): @View.nested class recent_jobs(Card): ROOT = './/article[.//div[text()="Recent jobs"]]' - is_table_loaded = './/ul[@aria-label="recent-jobs-table"]' actions = Dropdown(locator='.//div[contains(@class, "pf-c-dropdown")]') class finished(Tab): - table = ItemList() + table = SatTableWithoutHeaders(locator='.//table[@aria-label="recent-jobs-table"]') class running(Tab): - table = ItemList() + table = SatTableWithoutHeaders(locator='.//table[@aria-label="recent-jobs-table"]') class scheduled(Tab): - table = ItemList() + table = SatTableWithoutHeaders(locator='.//table[@aria-label="recent-jobs-table"]') @View.nested class content(Tab): From e824619ea737a82ca7e3d3a4cc499d2423a790cb Mon Sep 17 00:00:00 2001 From: vsedmik <46570670+vsedmik@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:34:21 -0400 Subject: [PATCH 14/35] Rename column in puppet class table. (#758) The change was introduced in 6.12 with transition to PF4. --- airgun/entities/puppet_class.py | 4 ++-- airgun/views/puppet_class.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/airgun/entities/puppet_class.py b/airgun/entities/puppet_class.py index 8caa05142..2e7a2313c 100644 --- a/airgun/entities/puppet_class.py +++ b/airgun/entities/puppet_class.py @@ -38,7 +38,7 @@ def delete(self, entity_name): """Delete puppet class entity""" view = self.navigate_to(self, 'All') view.search(entity_name) - view.table.row(class_name=entity_name)['Actions'].widget.click(handle_alert=True) + view.table.row(name=entity_name)['Actions'].widget.click(handle_alert=True) view.flash.assert_no_error() view.flash.dismiss() @@ -70,4 +70,4 @@ def prerequisite(self, *args, **kwargs): def step(self, *args, **kwargs): entity_name = kwargs.get('entity_name') self.parent.search(entity_name) - self.parent.table.row(class_name=entity_name)['Class name'].widget.click() + self.parent.table.row(name=entity_name)['Name'].widget.click() diff --git a/airgun/views/puppet_class.py b/airgun/views/puppet_class.py index e91b1c8a7..45d560393 100644 --- a/airgun/views/puppet_class.py +++ b/airgun/views/puppet_class.py @@ -19,7 +19,7 @@ class PuppetClassesView(BaseLoggedInView, SearchableViewMixin): table = SatTable( './/table', column_widgets={ - 'Class name': Text('./a'), + 'Name': Text('./a'), 'Actions': Text('.//a[@data-method="delete"]'), }, ) From e55b132fde4252773b145f52e53d26cac6d4bf11 Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Tue, 20 Sep 2022 18:05:33 +0530 Subject: [PATCH 15/35] fixing the locator for logout page rhsso (#757) --- airgun/views/rhsso_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/views/rhsso_login.py b/airgun/views/rhsso_login.py index 7b56ef4e9..be1fbb67a 100644 --- a/airgun/views/rhsso_login.py +++ b/airgun/views/rhsso_login.py @@ -16,7 +16,7 @@ def is_displayed(self): class RhssoExternalLogoutView(View, ClickableMixin): login_again = Text('//a[@href="/users/extlogin"]') - logo = Text('//img[@alt="logo') + logo = Text('//img[@alt="logo"]') @property def is_displayed(self): From c9f456e042af834497a5462d02f17db2cbfe5c8c Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Tue, 20 Sep 2022 14:37:35 +0200 Subject: [PATCH 16/35] add static OUIA id for close button (#755) --- airgun/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/widgets.py b/airgun/widgets.py index 40307c3a7..95949ea00 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -1181,7 +1181,7 @@ class Pf4ConfirmationDialog(ConfirmationDialog): ROOT = '//div[@id="app-confirm-modal" or @data-ouia-component-type="PF4/ModalContent"]' confirm_dialog = PF4Button('btn-modal-confirm') cancel_dialog = PF4Button('btn-modal-cancel') - discard_dialog = PF4Button('OUIA-Generated-Modal-small-1-ModalBoxCloseButton') + discard_dialog = PF4Button('app-confirm-modal-ModalBoxCloseButton') class LCESelector(GenericLocatorWidget): From e736ecd9dc03a47f9f709fdc08e4a45009218f5e Mon Sep 17 00:00:00 2001 From: Adarsh dubey Date: Tue, 20 Sep 2022 18:10:20 +0530 Subject: [PATCH 17/35] Locator fix for errata (#754) --- airgun/views/contenthost.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/airgun/views/contenthost.py b/airgun/views/contenthost.py index 43b2b184a..9dc2a298a 100644 --- a/airgun/views/contenthost.py +++ b/airgun/views/contenthost.py @@ -89,8 +89,6 @@ class errata(View): bug_fix = Text(".//span[contains(@ng-class, 'errataCounts.bugfix')]") enhancement = Text(".//span[contains(@ng-class, 'errataCounts.enhancement')]") - packages = Text("./a[contains(@ui-sref, 'packages.applicable')]") - class ContentHostsView(BaseLoggedInView, SearchableViewMixin): title = Text("//h2[contains(., 'Content Hosts')]") @@ -236,7 +234,7 @@ class packages_applicable(SatTabWithDropdown, SearchableViewMixin): class errata(SatTab): lce_filter = Select(locator='.//select[@ng-model="selectedErrataOption"]') searchbox = Search() - apply_selected = ActionsDropdown(".//span[contains(@class, 'btn-group')]") + apply_selected = ActionsDropdown(".//span[contains(@class, 'btn-group dropdown')]") recalculate = Button('Recalculate') table = SatTable( './/table', From 9035bfd8006254eb7479ec9d03503fb4ac9f4b96 Mon Sep 17 00:00:00 2001 From: Griffin Sullivan <48397354+Griffin-Sullivan@users.noreply.github.com> Date: Thu, 22 Sep 2022 03:28:43 -0400 Subject: [PATCH 18/35] Fixing new Webhook button (#761) Co-authored-by: Griffin Sullivan --- airgun/entities/webhook.py | 6 +----- airgun/views/webhook.py | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/airgun/entities/webhook.py b/airgun/entities/webhook.py index af3a837fd..33280d2cc 100644 --- a/airgun/entities/webhook.py +++ b/airgun/entities/webhook.py @@ -1,5 +1,4 @@ from navmazing import NavigateToSibling -from widgetastic.exceptions import NoSuchElementException from airgun.entities.base import BaseEntity from airgun.navigation import NavigateStep @@ -90,10 +89,7 @@ class AddNewWebhook(NavigateStep): prerequisite = NavigateToSibling('All') def step(self, *args, **kwargs): - try: - self.parent.new.click() - except NoSuchElementException: - self.parent.new_on_blank_page.click() + self.parent.new.click() @navigator.register(WebhookEntity, 'Edit') diff --git a/airgun/views/webhook.py b/airgun/views/webhook.py index efc712cbc..56f14c907 100644 --- a/airgun/views/webhook.py +++ b/airgun/views/webhook.py @@ -15,8 +15,7 @@ class WebhooksView(BaseLoggedInView, SearchableViewMixin): title = Text("//h1[normalize-space(.)='Webhooks']") - new = Button('Create Webhook') - new_on_blank_page = PF4Button('Create Webhook') + new = PF4Button('Create new') table = SatTable( './/table', column_widgets={ From 526bb47ec3a84d504d6950d780e87d55522b7ef7 Mon Sep 17 00:00:00 2001 From: Lai Tran Date: Thu, 22 Sep 2022 03:36:43 -0400 Subject: [PATCH 19/35] Remove publish via http field since it's not longer valid (#762) --- airgun/views/repository.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/airgun/views/repository.py b/airgun/views/repository.py index 758253bc2..a23f25313 100644 --- a/airgun/views/repository.py +++ b/airgun/views/repository.py @@ -202,7 +202,6 @@ class DockerRepository(View): repo_name = ReadOnlyEntry(name='Name') verify_ssl = EditableEntryCheckbox(name='Verify SSL') upstream_authorization = AuthorizationEntry(name='Upstream Authorization') - publish_via_http = EditableEntryCheckbox(name='Publish via HTTP') http_proxy_policy = EditableEntrySelect(name='HTTP Proxy') proxy_policy = ConditionalSwitchableView(reference='http_proxy_policy') mirroring_policy = EditableEntrySelect(name='Mirroring Policy') @@ -224,7 +223,6 @@ class YumRepository(View): retain_package_versions = EditableEntry(name='Retain package versions') http_proxy_policy = EditableEntrySelect(name='HTTP Proxy') ignore_srpms = EditableEntryCheckbox(name='Ignore SRPMs') - publish_via_http = EditableEntryCheckbox(name='Publish via HTTP') unprotected = EditableEntryCheckbox(name='Unprotected') gpg_key = EditableEntrySelect(name='GPG Key') download_policy = EditableEntrySelect(name='Download Policy') @@ -244,7 +242,6 @@ class AnsibleCollectionRepository(View): requirements = EditableEntry(name='Requirements') verify_ssl = EditableEntryCheckbox(name='Verify SSL') upstream_authorization = AuthorizationEntry(name='Upstream Authorization') - publish_via_http = EditableEntryCheckbox(name='Publish via HTTP') upload_content = FileInput(name='content[]') upload = Text("//button[contains(., 'Upload')]") http_proxy_policy = EditableEntrySelect(name='HTTP Proxy') @@ -260,7 +257,6 @@ class OstreeRepository(View): upstream_url = EditableEntry(name='Upstream URL') verify_ssl = EditableEntryCheckbox(name='Verify SSL') upstream_authorization = AuthorizationEntry(name='Upstream Authorization') - publish_via_https = ReadOnlyEntry(name='Publish via HTTPS') published_at = ReadOnlyEntry(name='Published At') http_proxy_policy = EditableEntrySelect(name='HTTP Proxy') proxy_policy = ConditionalSwitchableView(reference='http_proxy_policy') @@ -274,7 +270,6 @@ class FileRepository(View): upstream_url = EditableEntry(name='Upstream URL') verify_ssl = EditableEntryCheckbox(name='Verify SSL') upstream_authorization = AuthorizationEntry(name='Upstream Authorization') - publish_via_http = EditableEntryCheckbox(name='Publish via HTTP') unprotected = EditableEntryCheckbox(name='Unprotected') mirroring_policy = EditableEntrySelect(name='Mirroring Policy') From d86d62e21060f8ab9e7d36c37628ad82d7d633a9 Mon Sep 17 00:00:00 2001 From: Lai Tran Date: Thu, 22 Sep 2022 14:00:42 -0400 Subject: [PATCH 20/35] Create cv and search for new CV UI (#749) --- airgun/entities/contentview_new.py | 46 ++++++++++++++++++++++ airgun/session.py | 6 +++ airgun/views/common.py | 43 ++++++++++++++++++++ airgun/views/contentview_new.py | 63 ++++++++++++++++++++++++++++++ airgun/widgets.py | 26 ++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 airgun/entities/contentview_new.py create mode 100644 airgun/views/contentview_new.py diff --git a/airgun/entities/contentview_new.py b/airgun/entities/contentview_new.py new file mode 100644 index 000000000..138ff313f --- /dev/null +++ b/airgun/entities/contentview_new.py @@ -0,0 +1,46 @@ +from navmazing import NavigateToSibling + +from airgun.entities.base import BaseEntity +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 + + +class NewContentViewEntity(BaseEntity): + endpoint_path = '/content_views' + + def create(self, values): + """Create a new content view""" + view = self.navigate_to(self, 'New') + view.fill(values) + view.submit.click() + + def search(self, value): + """Search for content view""" + view = self.navigate_to(self, 'All') + return view.search(value) + + +@navigator.register(NewContentViewEntity, 'All') +class ShowAllContentViewsScreen(NavigateStep): + """Navigate to All Content Views screen.""" + + VIEW = NewContentViewTableView + + @retry_navigation + def step(self, *args, **kwargs): + self.view.menu.select('Content', 'Content Views') + + +@navigator.register(NewContentViewEntity, 'New') +class CreateContentView(NavigateStep): + """Navigate to Create content view.""" + + VIEW = NewContentViewCreateView + + prerequisite = NavigateToSibling('All') + + def step(self, *args, **kwargs): + self.parent.create_content_view.click() diff --git a/airgun/session.py b/airgun/session.py index 1050d312e..37f34cc48 100644 --- a/airgun/session.py +++ b/airgun/session.py @@ -25,6 +25,7 @@ from airgun.entities.contentcredential import ContentCredentialEntity from airgun.entities.contenthost import ContentHostEntity from airgun.entities.contentview import ContentViewEntity +from airgun.entities.contentview_new import NewContentViewEntity from airgun.entities.contentviewfilter import ContentViewFilterEntity from airgun.entities.dashboard import DashboardEntity from airgun.entities.discoveredhosts import DiscoveredHostsEntity @@ -388,6 +389,11 @@ def contentview(self): """Instance of Content View entity.""" return self._open(ContentViewEntity) + @cached_property + def contentview_new(self): + """Instance of the New Content View entity.""" + return self._open(NewContentViewEntity) + @cached_property def contentviewfilter(self): """Instance of Content View Filter entity.""" diff --git a/airgun/views/common.py b/airgun/views/common.py index 78c3084b1..7154a2ceb 100644 --- a/airgun/views/common.py +++ b/airgun/views/common.py @@ -22,6 +22,7 @@ from airgun.widgets import ItemsList from airgun.widgets import LCESelector from airgun.widgets import Pf4ConfirmationDialog +from airgun.widgets import PF4Search from airgun.widgets import ProgressBar from airgun.widgets import ReadOnlyEntry from airgun.widgets import SatFlashMessages @@ -445,6 +446,48 @@ def search(self, query): return self.table.read() +class SearchableViewMixinPF4(SearchableViewMixin): + """Mixin which adds :class:`airgun.widgets.Search` widget and + :meth:`airgun.widgets.Search.search` to your view. It's useful for _most_ entities list views + + where searchbox and results table are present. + Note that class which uses this mixin should have :attr: `table` attribute. + """ + + searchbox = PF4Search() + blank_page = Text("//div[contains(@class, 'pf-c-empty-state')]") + + def is_searchable(self): + """Verify that search procedure can be executed against specific page + that is not blank + """ + if self.searchbox.search_field.is_displayed and (not self.blank_page.is_displayed): + return True + return False + + def search(self, query): + """Perform search using searchbox on the page and return table + contents. + + :param str query: search query to type into search field. E.g. ``foo`` + or ``name = "bar"``. + :return: list of dicts representing table rows + :rtype: list + """ + if not hasattr(self.__class__, 'table'): + raise AttributeError( + f'Class {self.__class__.__name__} does not have attribute "table". ' + 'SearchableViewMixin only works with views, which have table for results. ' + 'Please define table or use custom search implementation instead' + ) + if not self.is_searchable(): + return None + self.searchbox.search(query) + self.browser.plugin.ensure_page_safe(timeout='60s') + self.table.wait_displayed() + return self.table.read() + + class TaskDetailsView(BaseLoggedInView): """Common view for task details screen. Can be found for most of tasks for various entities like Products, Repositories, Errata etc. diff --git a/airgun/views/contentview_new.py b/airgun/views/contentview_new.py new file mode 100644 index 000000000..71c64d558 --- /dev/null +++ b/airgun/views/contentview_new.py @@ -0,0 +1,63 @@ +from widgetastic.widget import Checkbox +from widgetastic.widget import Text +from widgetastic.widget import TextInput +from widgetastic.widget import View +from widgetastic_patternfly4.ouia import Button as PF4Button +from widgetastic_patternfly4.ouia import ExpandableTable + +from airgun.views.common import BaseLoggedInView +from airgun.views.common import SearchableViewMixinPF4 + + +class NewContentViewTableView(BaseLoggedInView, SearchableViewMixinPF4): + title = Text('.//h1[@data-ouia-component-id="cvPageHeaderText"]') + create_content_view = PF4Button('create-content-view') + table = ExpandableTable( + component_id='content-views-table', + column_widgets={ + 'Name': Text('./a'), + 'Last task': Text('.//a'), + 'Latest version': Text('.//a'), + }, + ) + + @property + def is_displayed(self): + assert self.create_content_view.is_displayed() + return True + + +class NewContentViewCreateView(BaseLoggedInView): + title = Text('.//div[@data-ouia-component-id="create-content-view-modal"]') + name = TextInput(id='name') + label = TextInput(id='label') + description = TextInput(id='description') + 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() + + @property + def is_displayed(self): + self.title.is_displayed() + self.label.is_displayed() + return True + + def after_fill(self, value): + """Ensure 'Create content view' button is enabled after filling out the required fields""" + self.submit.wait_displayed() diff --git a/airgun/widgets.py b/airgun/widgets.py index 95949ea00..b89ede0ac 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -746,6 +746,32 @@ def search(self, value): self.search_button.click() +class PF4Search(Search): + """PF4 Searchbar for table filtering""" + + ROOT = '//div[@role="combobox" or @aria-haspopup="listbox"]' + search_field = TextInput( + locator=( + ".//input[@type='text' or @id='downshift-0-input' or" + " contains(@class, 'pf-m-search') or data-ouia-component-type='PF4/TextInput']" + ) + ) + clear_button = Button(locator=".//button[contains(@class,'search-clear')]") + + def clear(self): + """Clears search field value and re-trigger search to remove all + filters. + """ + if self.clear_button.is_displayed: + self.clear_button.click() + else: + self.browser.clear(self.search_field) + + def search(self, value): + self.clear() + self.fill(value) + + class SatVerticalNavigation(VerticalNavigation): """The Patternfly Vertical navigation.""" From a143737567650eb16c2da76b5f20b3f594333afb Mon Sep 17 00:00:00 2001 From: Lai Tran Date: Fri, 23 Sep 2022 12:02:54 -0400 Subject: [PATCH 21/35] Missing delete confirmation function (#763) --- airgun/entities/lifecycleenvironment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/airgun/entities/lifecycleenvironment.py b/airgun/entities/lifecycleenvironment.py index 192e1fd56..fe6c4c9e8 100644 --- a/airgun/entities/lifecycleenvironment.py +++ b/airgun/entities/lifecycleenvironment.py @@ -62,6 +62,7 @@ def delete(self, entity_name): """Deletes existing lifecycle environment entity""" view = self.navigate_to(self, 'Edit', entity_name=entity_name) view.remove.click() + self.browser.handle_alert() view.flash.assert_no_error() view.flash.dismiss() From 038fc7d16f5db6731e070a72972ec4521bcf53f3 Mon Sep 17 00:00:00 2001 From: Shweta Singh Date: Fri, 30 Sep 2022 20:39:26 +0530 Subject: [PATCH 22/35] Updated locator for Text field in Settings (#767) --- airgun/widgets.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/airgun/widgets.py b/airgun/widgets.py index b89ede0ac..579fb7a47 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -2318,7 +2318,11 @@ class PopOverWidget(View): """ ROOT = '.' - column_value = Text(".//div[contains(@class,'ellipsis-pf-tooltip editable')]") + column_value = Text( + './/div[contains(@class, "ellipsis-pf-tooltip editable") ' + 'or contains(@class,"ellipsis editable") ' + 'or contains(@class,"ellipsis editable-empty editable")]' + ) pop_over_view = PopOverModalView() def fill(self, item): From dac0e2097824f128c18fba9ac546f45d1bbd265e Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Mon, 3 Oct 2022 08:20:18 +0200 Subject: [PATCH 23/35] fix OUIA IDs (#766) --- airgun/views/host.py | 5 +++-- airgun/views/host_new.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/airgun/views/host.py b/airgun/views/host.py index 731fc2564..fe11be57a 100644 --- a/airgun/views/host.py +++ b/airgun/views/host.py @@ -521,7 +521,8 @@ class advanced(Tab): token_life_time = TextInput(id='reg_token_life_time_input') rex_interface = TextInput(id='reg_rex_interface_input') activation_keys = BaseMultiSelect('OUIA-Generated-Select-typeaheadmulti-1') - life_cycle_env = FormSelect('OUIA-Generated-FormSelect-default-8') + rex_pull_mode = FormSelect('OUIA-Generated-FormSelect-default-8') + life_cycle_env = FormSelect('OUIA-Generated-FormSelect-default-9') ignore_error = Checkbox(id='reg_katello_ignore') force = Checkbox(id='reg_katello_force') activation_key_helper = Text("//div[@id='reg_katello_ak-helper']") @@ -625,7 +626,7 @@ def read(self): class HostDetailsView(BaseLoggedInView): - breadcrumb = PF4BreadCrumb('OUIA-Generated-Breadcrumb-1') + breadcrumb = PF4BreadCrumb('breadcrumbs-list') @property def is_displayed(self): diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index 9013ef29c..b60b3e2ef 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -60,7 +60,7 @@ def read(self): class NewHostDetailsView(BaseLoggedInView): - breadcrumb = BreadCrumb('OUIA-Generated-Breadcrumb-1') + breadcrumb = BreadCrumb('breadcrumbs-list') @property def is_displayed(self): From b37f029449c79a0bfbe226d6f261fbccddec626b Mon Sep 17 00:00:00 2001 From: Shubham Ganar Date: Fri, 21 Oct 2022 14:09:19 +0530 Subject: [PATCH 24/35] Remove environment locator --- airgun/views/provisioning_template.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/airgun/views/provisioning_template.py b/airgun/views/provisioning_template.py index c66d9abdf..0742e45a4 100644 --- a/airgun/views/provisioning_template.py +++ b/airgun/views/provisioning_template.py @@ -24,7 +24,6 @@ class TemplateHostEnvironmentAssociation(GenericRemovableWidgetItem): remove_button = Text(".//a[@title='Remove Combination']") host_group = Select(locator=".//select[contains(@name, '[hostgroup_id]')]") - environment = Select(locator=".//select[contains(@name, '[environment_id]')]") class ProvisioningTemplatesView(BaseLoggedInView, SearchableViewMixin): @@ -81,7 +80,7 @@ class association(SatTab): applicable_os = MultiSelect(id='ms-provisioning_template_operatingsystem_ids') @View.nested - class hg_environment_combination(RemovableWidgetsItemsListView): + class vaild_hostgroups(RemovableWidgetsItemsListView): ROOT = "//div[@id='association']" ITEMS = ".//fieldset[@id='template_combination']/div" ITEM_WIDGET_CLASS = TemplateHostEnvironmentAssociation From 041711667ec1a362c2638a7943dcdcfffa6d43a5 Mon Sep 17 00:00:00 2001 From: Omkar Khatavkar Date: Wed, 26 Oct 2022 15:16:11 +0530 Subject: [PATCH 25/35] added the support for the clone hostgroup (#773) --- airgun/entities/hostgroup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/airgun/entities/hostgroup.py b/airgun/entities/hostgroup.py index a8bd384a6..6bbcef9fd 100644 --- a/airgun/entities/hostgroup.py +++ b/airgun/entities/hostgroup.py @@ -21,6 +21,17 @@ def create(self, values): view.flash.assert_no_error() view.flash.dismiss() + def clone(self, entity_name, values): + """Clone an existing host group entity""" + view = self.navigate_to(self, 'All') + view.search(entity_name) + view.table.row(name=entity_name)['Actions'].widget.fill('Clone') + view = HostGroupCreateView(self.browser) + view.fill(values) + view.submit.click() + view.flash.assert_no_error() + view.flash.dismiss() + def search(self, value): """Search for existing host group entity""" view = self.navigate_to(self, 'All') From 91427461e1e36f956f03022092ba90999aa1f885 Mon Sep 17 00:00:00 2001 From: Adarsh dubey Date: Fri, 28 Oct 2022 13:22:29 +0530 Subject: [PATCH 26/35] Uncommenting select_all_hits (#771) Signed-off-by: Adarsh Dubey Signed-off-by: Adarsh Dubey --- airgun/entities/cloud_insights.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/entities/cloud_insights.py b/airgun/entities/cloud_insights.py index a0e6785da..d997da3be 100644 --- a/airgun/entities/cloud_insights.py +++ b/airgun/entities/cloud_insights.py @@ -23,7 +23,7 @@ def remediate(self, entity_name): view = self.navigate_to(self, 'All') view.search(entity_name) view.select_all.fill(True) - # view.select_all_hits.click() skip till BZ#1975321 is fixed. + view.select_all_hits.click() view.remediate.click() view.remediation_window.remediate.click() self.run_job() From 7e4422282eb45503c38220a1859f1152da6e44d5 Mon Sep 17 00:00:00 2001 From: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> Date: Fri, 28 Oct 2022 14:15:54 +0530 Subject: [PATCH 27/35] Fix typo (#778) --- airgun/views/provisioning_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/views/provisioning_template.py b/airgun/views/provisioning_template.py index 0742e45a4..b368138d1 100644 --- a/airgun/views/provisioning_template.py +++ b/airgun/views/provisioning_template.py @@ -80,7 +80,7 @@ class association(SatTab): applicable_os = MultiSelect(id='ms-provisioning_template_operatingsystem_ids') @View.nested - class vaild_hostgroups(RemovableWidgetsItemsListView): + class valid_hostgroups(RemovableWidgetsItemsListView): ROOT = "//div[@id='association']" ITEMS = ".//fieldset[@id='template_combination']/div" ITEM_WIDGET_CLASS = TemplateHostEnvironmentAssociation From 007fe8bac68a980cd0ba5555ea535afda93b05df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hellebrandt?= Date: Thu, 3 Nov 2022 15:18:41 +0100 Subject: [PATCH 28/35] Only search search bar in the active element, not from DOM root (#772) --- airgun/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/widgets.py b/airgun/widgets.py index 579fb7a47..dea31ab5e 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -696,7 +696,7 @@ class Search(Widget): """Searchbar for table filtering""" ROOT = ( - '//div[contains(@class, "toolbar-pf-filter") or contains(@class, "title_filter")' + './/div[contains(@class, "toolbar-pf-filter") or contains(@class, "title_filter")' 'or contains(@class, "dataTables_filter") or @id="search-bar"]' ) search_field = TextInput( From 5e52906b921a5d66f73f1933553ea221a6ab0e56 Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Mon, 14 Nov 2022 10:36:30 +0100 Subject: [PATCH 29/35] change status id to static one (#781) --- airgun/views/host_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index b60b3e2ef..51efb3c67 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -83,7 +83,7 @@ class details(Card): @View.nested class host_status(Card): ROOT = './/article[.//span[text()="Host status"]]' - status = Text('.//h4[contains(@data-ouia-component-id, "OUIA-Generated-Title")]') + status = Text('.//h4[contains(@data-ouia-component-id, "global-state-title")]') status_success = Text('.//a[span[@class="status-success"]]') status_warning = Text('.//a[span[@class="status-warning"]]') From 84ecce47fdc9aa41a16bb84a46d98401ccfa6648 Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Mon, 14 Nov 2022 18:27:54 +0530 Subject: [PATCH 30/35] Alligning ACP GHA with Robottelo (#780) --- .github/auto_assign.yml | 1 + .github/workflows/auto_assignment.yaml | 21 ++++++++++++ .github/workflows/auto_cherry_pick.yml | 46 ++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 .github/auto_assign.yml create mode 100644 .github/workflows/auto_assignment.yaml diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 000000000..717af2a0c --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1 @@ +addAssignees: author diff --git a/.github/workflows/auto_assignment.yaml b/.github/workflows/auto_assignment.yaml new file mode 100644 index 000000000..328e4bf6f --- /dev/null +++ b/.github/workflows/auto_assignment.yaml @@ -0,0 +1,21 @@ +name: 'Auto Assign' + +on: + pull_request_target: + types: + - opened + - ready_for_review + - reopened + - synchronize + + +jobs: + add-assignees: # This needed to create the gh issue in case of failed auto-cherry-pick + name: Add author to assignee + if: "!contains(github.event.pull_request.labels.*.name, 'Auto_Cherry_Picked')" + runs-on: ubuntu-latest + steps: + - uses: kentaro-m/auto-assign-action@v1.2.4 + with: + repo-token: "${{ secrets.CHERRYPICK_PAT || github.token }}" + configuration-path: ".github/auto_assign.yml" diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index b173f675c..cdb3ecbe7 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -18,23 +18,57 @@ jobs: with: fetch-depth: 0 - id: set-branch - run: echo "::set-output name=previous_branch::$(if [ $GITHUB_BASE_REF == 'master' ]; then echo $(git branch -rl 'origin/6.*.z' --format='%(refname:lstrip=-1)' | sort --version-sort | tail -n1 | jq -cnR '[inputs | select(length>0)]'); else echo ['"6.'$(($(echo $GITHUB_BASE_REF | cut -d. -f2) - 1))'.z"']; fi)" + run: echo "previous_branch=$(if [ $GITHUB_BASE_REF == 'master' ]; then echo $(git branch -rl 'origin/6.*.z' --format='%(refname:lstrip=-1)' | sort --version-sort | tail -n1); else echo '6.'$(($(echo $GITHUB_BASE_REF | cut -d. -f2) - 1))'.z'; fi)" >> $GITHUB_OUTPUT auto-cherry-pick: name: Auto Cherry Pick to previous branch if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') needs: previous-branch runs-on: ubuntu-latest - strategy: - matrix: - to_branch: ${{ fromJson(needs.previous-branch.outputs.previous_branch) }} + env: + TO_BRANCH: ${{ needs.previous-branch.outputs.previous_branch }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Cherry pick into ${{ matrix.to_branch }} + - name: Cherry pick into ${{ env.TO_BRANCH }} uses: jyejare/github-cherry-pick-action@main with: - branch: ${{ matrix.to_branch }} + token: ${{ secrets.CHERRYPICK_PAT }} + branch: ${{ env.TO_BRANCH }} labels: | Auto_Cherry_Picked + ${{ env.TO_BRANCH }} + assignees: "${{ github.event.pull_request.assignee.login }}" + + create-issue: + runs-on: ubuntu-latest + if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }} + needs: [previous-branch, auto-cherry-pick] + env: + TO_BRANCH: ${{ needs.previous-branch.outputs.previous_branch }} + steps: + - name: Create Issue on Failed Auto Cherrypick + uses: dacbd/create-issue-action@main + with: + token: ${{ secrets.CHERRYPICK_PAT }} + title: "[Failed-AutoCherryPick] - ${{ github.event.pull_request.title }}" + body: | + #### Auto-Cherry-Pick WorkFlow Failure: + - To Branch: ${{ env.TO_BRANCH }} + - [Failed Cherrypick Action](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + - [Parent Pull Request](https://github.com/${{ github.repository }}/pull/${{ github.event.number }}) + labels: Failed_AutoCherryPick,${{ env.TO_BRANCH }} + assignees: "${{ github.event.pull_request.assignee.login }}" + + google-chat-notification: + runs-on: ubuntu-latest + if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }} + needs: auto-cherry-pick + steps: + - name: Google Chat Notification - Airgun + uses: Co-qn/google-chat-notification@releases/v1 + with: + name: "${{ github.event.pull_request.title }}" + url: ${{ secrets.GCHAT_REVIEWERS_WEBHOOK }} + status: failure From 8012321f79955301f6cf42d4cb8c3d4617f0e215 Mon Sep 17 00:00:00 2001 From: Jake Callahan Date: Thu, 24 Nov 2022 03:39:39 -0500 Subject: [PATCH 31/35] Switch classifiers from a tuple to a list (#784) * Switch classifiers from a tuple to a list Recommended by @abravalheri in https://github.com/pypa/setuptools/issues/3707#issuecomment-1325605870 * Fix github URL for pycqa/flake8 precommit hook Co-authored-by: jyejare --- .pre-commit-config.yaml | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c482f053e..ed9dbb968 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: rev: 22.3.0 hooks: - id: black -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 diff --git a/setup.py b/setup.py index 5d2e147db..51995ecb1 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ include_package_data=True, license='GNU GPL v3.0', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=( + classifiers=[ 'Development Status :: 1 - Planning', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', @@ -42,5 +42,5 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', - ), + ], ) From da1343154d0080db926e07db8ba93db4afac9d70 Mon Sep 17 00:00:00 2001 From: Samuel Bible Date: Mon, 28 Nov 2022 23:51:38 -0600 Subject: [PATCH 32/35] Fixes for Robottelo Role/Variable UI tests (#775) * Fixes for Robottelo Role/Variable UI tests * Fix pagination widget to be visible to is_displayed again * Fix more locators for Pagination widget * Update airgun/widgets.py Co-authored-by: synkd <48261305+synkd@users.noreply.github.com> * Update airgun/widgets.py Co-authored-by: synkd <48261305+synkd@users.noreply.github.com> Co-authored-by: synkd <48261305+synkd@users.noreply.github.com> --- airgun/views/ansible_role.py | 6 +++--- airgun/views/ansible_variable.py | 2 +- airgun/widgets.py | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/airgun/views/ansible_role.py b/airgun/views/ansible_role.py index 0f281fee2..e5d240bac 100644 --- a/airgun/views/ansible_role.py +++ b/airgun/views/ansible_role.py @@ -21,10 +21,10 @@ class AnsibleRolesView(BaseLoggedInView, SearchableViewMixin): is present, without the search widget or table. """ - title = Text("//h1[contains(., text()='Ansible Roles')") + title = Text("//h1") import_button = Text("//a[contains(@href, '/ansible_roles/import')]") submit = Button('Submit') - total_imported_roles = Text(".//span[contains(@class, 'pagination-pf-items-total')]") + total_imported_roles = Text("//span[@class='pf-c-options-menu__toggle-text']/b[2]") table = Table( './/table', column_widgets={ @@ -42,7 +42,7 @@ class AnsibleRolesImportView(BaseLoggedInView): """View while selecting Ansible roles to import.""" breadcrumb = BreadCrumb() - total_available_roles = Text("//div[@class='pf-c-pagination__total-items']/b[2]") + total_available_roles = Text("//span[@class='pf-c-options-menu__toggle-text']/b[2]") select_all = Checkbox(locator="//input[@id='select-all']") table = PatternflyTable( component_id='OUIA-Generated-Table-2', diff --git a/airgun/views/ansible_variable.py b/airgun/views/ansible_variable.py index 73dfdb4cc..dd0100f44 100644 --- a/airgun/views/ansible_variable.py +++ b/airgun/views/ansible_variable.py @@ -19,7 +19,7 @@ class AnsibleVariablesView(BaseLoggedInView, SearchableViewMixin): title = Text("//h1[contains(., text()='Ansible Variables')") new_variable = Text("//a[contains(@href, '/ansible/ansible_variables/new')]") - total_variables = Text(".//span[@class='pagination-pf-items-total']") + total_variables = Text("//span[@class='pf-c-options-menu__toggle-text']/b[2]") table = SatTable( './/table', column_widgets={ diff --git a/airgun/widgets.py b/airgun/widgets.py index dea31ab5e..820a69337 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -1588,17 +1588,17 @@ class Pagination(Widget): and current page index/overall amount of pages. Mainly used with Table widget. """ - ROOT = ".//form[contains(@class, 'content-view-pf-pagination')]" + ROOT = ".//foreman-react-component[contains(@name, 'Pagination')]" # Kattelo views use per_page with select, foreman use a per_page with Button DropDown. - PER_PAGE_BUTTON_DROPDOWN = ".//div[button[@id='pagination-row-dropdown']]" + PER_PAGE_BUTTON_DROPDOWN = ".//div[button[@id='paginationoptions-menu-toggle-3']]" PER_PAGE_SELECT = ".//select[contains(@ng-model, 'per_page')]" - first_page_button = Text(".//li[a[span[contains(@class, 'angle-double-left')]]]") - previous_page_button = Text(".//li[a[span[contains(@class, 'angle-left')]]]") - next_page_button = Text(".//li[a[span[contains(@class, 'angle-right')]]]") - last_page_button = Text(".//li[a[span[contains(@class, 'angle-double-right')]]]") - page = TextInput(locator=".//input[contains(@class, 'pagination-pf-page')]") - pages = Text(".//span[contains(@class, 'pagination-pf-pages')]") - total_items = Text(".//span[contains(@class, 'pagination-pf-items-total')]") + first_page_button = Button(".//div[button[@data-action='first']]") + previous_page_button = Button(".//div[button[@data-action='previous']]") + next_page_button = Button(".//div[button[@data-action='next']]") + last_page_button = Button(".//div[button[@data-action='last']]") + page = TextInput(locator=".//input[contains(@class, 'pf-c-form-control')]") + pages = Text("//div[contains(@class, 'pf-c-pagination__nav-page-select')]//span") + total_items = Text(".//span[contains(@class, 'pf-c-options-menu__toggle-text')]/b[2]") @cached_property def per_page(self): From 68debeaae3e4fb2c44e7a6fcc058080025aa340b Mon Sep 17 00:00:00 2001 From: Peter Dragun <43444182+peterdragun@users.noreply.github.com> Date: Tue, 29 Nov 2022 14:11:05 +0100 Subject: [PATCH 33/35] add repository sets entities (#770) --- airgun/entities/host_new.py | 23 ++++++++++++++++++++++- airgun/views/host_new.py | 23 ++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/airgun/entities/host_new.py b/airgun/entities/host_new.py index c2652a455..4bfcc4b2b 100644 --- a/airgun/entities/host_new.py +++ b/airgun/entities/host_new.py @@ -45,6 +45,7 @@ def get_packages(self, entity_name, search=""): view.content.packages.searchbar.fill(search) # wait for filter to apply self.browser.plugin.ensure_page_safe() + view.content.packages.table.wait_displayed() return view.content.packages.read() def install_package(self, entity_name, package): @@ -80,6 +81,8 @@ def get_errata_by_type(self, entity_name, type): view.wait_displayed() view.content.errata.select() view.content.errata.type_filter.fill(type) + self.browser.plugin.ensure_page_safe() + view.content.errata.table.wait_displayed() return view.read(widget_names="content.errata.table") def apply_erratas(self, entity_name, search): @@ -101,7 +104,7 @@ def get_module_streams(self, entity_name, search): view.content.module_streams.select() view.content.module_streams.searchbar.fill(search) # wait for filter to apply - self.browser.plugin.ensure_page_safe() + self.browser.wait_for_element(locator='//h4[text()="Loading"]', exception=False) view.content.module_streams.table.wait_displayed() return view.content.module_streams.table.read() @@ -119,6 +122,24 @@ def apply_module_streams_action(self, entity_name, module_stream, action): view.flash.assert_no_error() view.flash.dismiss() + def get_repo_sets(self, entity_name, search): + """Get all repository sets available for host""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.repository_sets.searchbar.fill(search) + self.browser.plugin.ensure_page_safe() + return view.content.repository_sets.table.read() + + def override_repo_sets(self, entity_name, repo_set, action): + """Change override for repository set""" + view = self.navigate_to(self, 'NewDetails', entity_name=entity_name) + view.wait_displayed() + view.content.repository_sets.searchbar.fill(repo_set) + self.browser.plugin.ensure_page_safe() + view.content.repository_sets.table[0][5].widget.item_select(action) + view.flash.assert_no_error() + view.flash.dismiss() + @navigator.register(NewHostEntity, 'NewDetails') class ShowNewHostDetails(NavigateStep): diff --git a/airgun/views/host_new.py b/airgun/views/host_new.py index 51efb3c67..b76899297 100644 --- a/airgun/views/host_new.py +++ b/airgun/views/host_new.py @@ -1,3 +1,6 @@ +import time + +from selenium.webdriver.common.keys import Keys from widgetastic.widget import Checkbox from widgetastic.widget import Text from widgetastic.widget import TextInput @@ -20,6 +23,16 @@ from airgun.widgets import SatTableWithoutHeaders +class SearchInput(TextInput): + def fill(self, value): + changed = super().fill(value) + if changed: + # workaround for BZ #2140636 + time.sleep(1) + self.browser.send_keys(Keys.ENTER, self) + return changed + + class Card(View): """Each card in host view has it's own title with same locator""" @@ -139,7 +152,7 @@ class packages(Tab): ROOT = './/div[@id="packages-tab"]' select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') - searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + searchbar = SearchInput(locator='.//input[contains(@class, "pf-m-search")]') status_filter = Dropdown(locator='.//div[@aria-label="select Status container"]/div') upgrade = Pf4ActionsDropdown( locator='.//div[div/button[normalize-space(.)="Upgrade"]]' @@ -165,7 +178,7 @@ class errata(Tab): ROOT = './/div[@id="errata-tab"]' select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') - searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + searchbar = SearchInput(locator='.//input[contains(@class, "pf-m-search")]') type_filter = Select(locator='.//div[@aria-label="select Type container"]/div') severity_filter = Select(locator='.//div[@aria-label="select Severity container"]/div') apply = Pf4ActionsDropdown(locator='.//div[@aria-label="errata_dropdown"]') @@ -192,7 +205,7 @@ class module_streams(Tab): # workaround for BZ 2119076 ROOT = './/div[@id="modulestreams-tab"]' - searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + searchbar = SearchInput(locator='.//input[contains(@class, "pf-m-search")]') status_filter = Select(locator='.//div[@aria-label="select Status container"]/div') installation_status_filter = Select( locator='.//div[@aria-label="select Installation status container"]/div' @@ -219,7 +232,7 @@ class repository_sets(Tab): ROOT = './/div[@id="repo-sets-tab"]' select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') - searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + searchbar = SearchInput(locator='.//input[contains(@class, "pf-m-search")]') status_filter = Select(locator='.//div[@aria-label="select Status container"]/div') dropdown = Dropdown(locator='.//div[button[@aria-label="bulk_actions"]]') @@ -255,7 +268,7 @@ class InstallPackagesView(View): ROOT = './/div[@id="package-install-modal"]' select_all = Checkbox(locator='.//div[@id="selection-checkbox"]/div/label') - searchbar = TextInput(locator='.//input[contains(@class, "pf-m-search")]') + searchbar = SearchInput(locator='.//input[contains(@class, "pf-m-search")]') table = Table( locator='.//table[@aria-label="Content View Table"]', From 6a2e121356b9fbfca5fce89cde48548b46d2000d Mon Sep 17 00:00:00 2001 From: Jitendra Yejare Date: Thu, 15 Dec 2022 18:38:45 +0530 Subject: [PATCH 34/35] Cherrypicking based on branch labels added to the parent PR (#788) --- .github/workflows/auto_cherry_pick.yml | 80 ++++++++++++-------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/.github/workflows/auto_cherry_pick.yml b/.github/workflows/auto_cherry_pick.yml index cdb3ecbe7..2eddfdda0 100644 --- a/.github/workflows/auto_cherry_pick.yml +++ b/.github/workflows/auto_cherry_pick.yml @@ -6,69 +6,65 @@ on: types: - closed +# Github & Parent PR Env vars +env: + assignee: ${{ github.event.pull_request.assignee.login }} + title: ${{ github.event.pull_request.title }} + number: ${{ github.event.number }} + jobs: - previous-branch: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') - name: Calculate previous branch name - runs-on: ubuntu-latest - outputs: - previous_branch: ${{ steps.set-branch.outputs.previous_branch }} - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - id: set-branch - run: echo "previous_branch=$(if [ $GITHUB_BASE_REF == 'master' ]; then echo $(git branch -rl 'origin/6.*.z' --format='%(refname:lstrip=-1)' | sort --version-sort | tail -n1); else echo '6.'$(($(echo $GITHUB_BASE_REF | cut -d. -f2) - 1))'.z'; fi)" >> $GITHUB_OUTPUT + # Auto CherryPicking and Failure Recording auto-cherry-pick: - name: Auto Cherry Pick to previous branch if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CherryPick') - needs: previous-branch + name: Auto Cherry Pick to labeled branches runs-on: ubuntu-latest - env: - TO_BRANCH: ${{ needs.previous-branch.outputs.previous_branch }} + strategy: + matrix: + label: ${{ github.event.pull_request.labels.*.name }} + steps: + ## Nailgun Repo Checkout - uses: actions/checkout@v3 + if: ${{ startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} with: fetch-depth: 0 - - name: Cherry pick into ${{ env.TO_BRANCH }} + + ## CherryPicking and AutoMerging + - name: Cherrypicking to zStream branch + id: cherrypick + if: ${{ startsWith(matrix.label, '6.') && matrix.label != github.base_ref }} uses: jyejare/github-cherry-pick-action@main with: token: ${{ secrets.CHERRYPICK_PAT }} - branch: ${{ env.TO_BRANCH }} + branch: ${{ matrix.label }} labels: | Auto_Cherry_Picked - ${{ env.TO_BRANCH }} - assignees: "${{ github.event.pull_request.assignee.login }}" + ${{ matrix.label }} + assignees: ${{ env.assignee }} - create-issue: - runs-on: ubuntu-latest - if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }} - needs: [previous-branch, auto-cherry-pick] - env: - TO_BRANCH: ${{ needs.previous-branch.outputs.previous_branch }} - steps: - - name: Create Issue on Failed Auto Cherrypick + ## Failure Logging to issues and GChat Group + - name: Create Github issue on cherrypick failure + id: create-issue + if: ${{ always() && steps.cherrypick.outcome == 'failure' }} uses: dacbd/create-issue-action@main with: token: ${{ secrets.CHERRYPICK_PAT }} - title: "[Failed-AutoCherryPick] - ${{ github.event.pull_request.title }}" + title: "[Failed-AutoCherryPick] - ${{ env.title }}" body: | #### Auto-Cherry-Pick WorkFlow Failure: - - To Branch: ${{ env.TO_BRANCH }} + - To Branch: ${{ matrix.label }} - [Failed Cherrypick Action](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - - [Parent Pull Request](https://github.com/${{ github.repository }}/pull/${{ github.event.number }}) - labels: Failed_AutoCherryPick,${{ env.TO_BRANCH }} - assignees: "${{ github.event.pull_request.assignee.login }}" - - google-chat-notification: - runs-on: ubuntu-latest - if: ${{ always() && contains(join(needs.*.result, ','), 'failure') }} - needs: auto-cherry-pick - steps: - - name: Google Chat Notification - Airgun - uses: Co-qn/google-chat-notification@releases/v1 + - [Parent Pull Request](https://github.com/${{ github.repository }}/pull/${{ env.number }}) + labels: Failed_AutoCherryPick,${{ matrix.label }} + assignees: ${{ env.assignee }} + - name: Send Google Chat notification on cherrypick failure + id: google-chat-notification + if: ${{ always() && steps.cherrypick.outcome == 'failure' }} + uses: omkarkhatavkar/google-chat-notification@master with: - name: "${{ github.event.pull_request.title }}" + name: ${{ env.title }} url: ${{ secrets.GCHAT_REVIEWERS_WEBHOOK }} + issue_url: ${{ steps.create-issue.outputs.html_url }} + author: ${{ env.assignee }} status: failure From dc169b98410510e9fc1928d2bdca85bfe07a1790 Mon Sep 17 00:00:00 2001 From: Adarsh dubey Date: Mon, 19 Dec 2022 13:31:40 +0530 Subject: [PATCH 35/35] Ansible UI fixes (#790) * add repository sets entities (#770) * Ansible UI fixes Signed-off-by: Adarsh Dubey Signed-off-by: Adarsh Dubey Co-authored-by: Peter Dragun <43444182+peterdragun@users.noreply.github.com> --- airgun/entities/ansible_role.py | 7 +++---- airgun/views/ansible_role.py | 4 ++-- airgun/views/ansible_variable.py | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/airgun/entities/ansible_role.py b/airgun/entities/ansible_role.py index 966e0ac5a..49e7a2313 100644 --- a/airgun/entities/ansible_role.py +++ b/airgun/entities/ansible_role.py @@ -39,10 +39,9 @@ def imported_roles_count(self): view = self.navigate_to(self, 'All') # Before any roles have been imported, no table or pagination widget are # present on the page - if not view.pagination.is_displayed: - return 0 - else: - return int(view.total_imported_roles.read()) + # Applying wait_displayed for the page to get rendered + view.wait_displayed() + return int(view.total_imported_roles.read()) def import_all_roles(self): """Import all available roles and return the number of roles diff --git a/airgun/views/ansible_role.py b/airgun/views/ansible_role.py index e5d240bac..c9d367669 100644 --- a/airgun/views/ansible_role.py +++ b/airgun/views/ansible_role.py @@ -21,10 +21,10 @@ class AnsibleRolesView(BaseLoggedInView, SearchableViewMixin): is present, without the search widget or table. """ - title = Text("//h1") + title = Text("//h1[contains(normalize-space(.),'Ansible Roles')]") import_button = Text("//a[contains(@href, '/ansible_roles/import')]") submit = Button('Submit') - total_imported_roles = Text("//span[@class='pf-c-options-menu__toggle-text']/b[2]") + total_imported_roles = Text("//span[@class='pf-c-options-menu__toggle-text']//b[2]") table = Table( './/table', column_widgets={ diff --git a/airgun/views/ansible_variable.py b/airgun/views/ansible_variable.py index dd0100f44..0f4b446ce 100644 --- a/airgun/views/ansible_variable.py +++ b/airgun/views/ansible_variable.py @@ -17,9 +17,9 @@ class AnsibleVariablesView(BaseLoggedInView, SearchableViewMixin): """Main Ansible Variables view""" - title = Text("//h1[contains(., text()='Ansible Variables')") + title = Text("//h1[contains(normalize-space(.),'Ansible Variables')]") new_variable = Text("//a[contains(@href, '/ansible/ansible_variables/new')]") - total_variables = Text("//span[@class='pf-c-options-menu__toggle-text']/b[2]") + total_variables = Text("//span[@class='pf-c-options-menu__toggle-text']//b[2]") table = SatTable( './/table', column_widgets={