diff --git a/airgun/entities/webhook.py b/airgun/entities/webhook.py index 3c56f7e33..53135f8c4 100644 --- a/airgun/entities/webhook.py +++ b/airgun/entities/webhook.py @@ -43,7 +43,7 @@ def search(self, entity_name): view = self.navigate_to(self, 'All') return view.search(entity_name) - def read(self, entity_name, widget_names=None): + def read(self, entity_name): """Reads content of corresponding Webhook :param str entity_name: name of the corresponding Webhook @@ -52,7 +52,7 @@ def read(self, entity_name, widget_names=None): """ view = self.navigate_to(self, 'Edit', entity_name=entity_name) view.wait_for_popup() - result = view.read(widget_names=widget_names) + result = view.read() view.cancel_button.click() return result @@ -128,4 +128,5 @@ 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(name=entity_name)['Actions'].widget.click() + row = self.parent.table.row(name=entity_name) + self.parent.browser.element('.//button[contains(text(), "Delete")]', parent=row).click() diff --git a/airgun/views/webhook.py b/airgun/views/webhook.py index e325d497c..a66011123 100644 --- a/airgun/views/webhook.py +++ b/airgun/views/webhook.py @@ -1,10 +1,9 @@ -from wait_for import wait_for -from widgetastic.widget import Checkbox, Text, TextInput, View -from widgetastic_patternfly import Button -from widgetastic_patternfly5 import Button as PF5Button, Tab +from widgetastic.widget import Checkbox, Text, TextInput +from widgetastic_patternfly5 import Button as PF5Button +from widgetastic_patternfly5.ouia import Button as PF5OUIAButton from airgun.views.common import BaseLoggedInView, SearchableViewMixinPF4 -from airgun.widgets import AutoCompleteTextInput, SatTable +from airgun.widgets import PF5TypeaheadSelect, SatTable class WebhooksView(BaseLoggedInView, SearchableViewMixinPF4): @@ -13,8 +12,8 @@ class WebhooksView(BaseLoggedInView, SearchableViewMixinPF4): table = SatTable( './/table', column_widgets={ - 'Name': Text('//span[@type="button"]'), - 'Actions': Button('Delete'), + 'Name': PF5OUIAButton('name-edit-active-button'), + 'Actions': PF5Button(locator='.//button[contains(@id, "delete")]'), }, ) @@ -23,90 +22,109 @@ def is_displayed(self): return self.browser.wait_for_element(self.title, exception=False) is not None -class WebhookCreateView(BaseLoggedInView): - ROOT = '//div[@role="dialog" and @tabindex][div//h4]' - cancel_button = Button('Cancel') - submit_button = Button('contains', 'Submit') - - @View.nested - class general(Tab): - subscribe_to = AutoCompleteTextInput( - locator=( - "//div[@class='webhook-form-tab-content']" - "/div[label[normalize-space(.)='Subscribe to*']]/div/div/div/input" - ) - ) - name = TextInput(name='name') - target_url = TextInput(name='target_url') - template = AutoCompleteTextInput( - locator=( - "//div[@class='webhook-form-tab-content']" - "/div[label[normalize-space(.)='Template*']]/div/div/div/input" - ) - ) - http_method = AutoCompleteTextInput( - locator=( - "//div[@class='webhook-form-tab-content']" - "/div[label[normalize-space(.)='HTTP Method*']]/div/div/div/input" - ) - ) - enabled = Checkbox(name='enabled') - - @View.nested - class credentials(Tab): - user = TextInput(name='user') - password = TextInput(name='password') - verify_ssl = Checkbox(name='verify_ssl') - capsule_auth = Checkbox(name='proxy_authorization') - certs = TextInput(name='ssl_ca_certs') - - @View.nested - class additional(Tab): - content_type = TextInput(name='http_content_type') - headers = TextInput(name='http_headers') +class WebhookFormView(BaseLoggedInView): + """Base view for webhook create/edit forms.""" + + cancel_button = PF5Button('Cancel') + submit_button = PF5Button('Submit') + + # Tab buttons + general_tab = PF5OUIAButton('webhook-form-tab-general') + credentials_tab = PF5OUIAButton('webhook-form-tab-creds') + additional_tab = PF5OUIAButton('webhook-form-tab-add') + + # General tab fields + subscribe_to = PF5TypeaheadSelect(locator='//input[@id="id-event"]') + name = TextInput(locator='//input[@id="id-name"]') + target_url = TextInput(locator='//input[@id="id-target_url"]') + template = PF5TypeaheadSelect(locator='//input[@id="id-webhook_template_id"]') + http_method = PF5TypeaheadSelect(locator='//input[@id="id-http_method"]') + enabled = Checkbox(id='id-enabled') + + # Credentials tab fields + user = TextInput(locator='//input[@id="id-user"]') + password = TextInput(locator='//input[@id="id-password"]') + verify_ssl = Checkbox(id='id-verify_ssl') + capsule_auth = Checkbox(id='id-proxy_authorization') + certs = TextInput(locator='//textarea[@id="id-ssl_ca_certs"]') + + # Additional tab fields + content_type = TextInput(locator='//input[@id="id-http_content_type"]') + headers = TextInput(locator='//textarea[@id="id-http_headers"]') + + def _switch_to_tab(self, tab_name): + """Click tab button to switch tabs.""" + tab_buttons = { + 'general': self.general_tab, + 'credentials': self.credentials_tab, + 'additional': self.additional_tab, + } + tab_buttons[tab_name].click() + self.browser.plugin.ensure_page_safe() + + def fill(self, values): + """Fill form values. Expects {'tab.field': value} format.""" + tabs_to_fill = {'general': {}, 'credentials': {}, 'additional': {}} + for key, value in values.items(): + tab_name, field_name = key.split('.', 1) + tabs_to_fill[tab_name][field_name] = value + + for tab_name in ['general', 'credentials', 'additional']: + if tabs_to_fill[tab_name]: + self._switch_to_tab(tab_name) + for field_name, value in tabs_to_fill[tab_name].items(): + getattr(self, field_name).fill(value) + + def read(self): + """Read form values from all tabs.""" + result = {'general': {}, 'credentials': {}, 'additional': {}} + fields = { + 'general': ['subscribe_to', 'name', 'target_url', 'template', 'http_method', 'enabled'], + 'credentials': ['user', 'password', 'verify_ssl', 'capsule_auth', 'certs'], + 'additional': ['content_type', 'headers'], + } + for tab_name, field_list in fields.items(): + self._switch_to_tab(tab_name) + for field_name in field_list: + widget = getattr(self, field_name) + result[tab_name][field_name] = widget.read() if widget.is_displayed else None + return result @property def is_displayed(self): - return self.browser.wait_for_element( - locator=self.cancel_button, visible=True, exception=True - ) is not None and 'in' in self.browser.classes(self) + return ( + self.browser.wait_for_element(self.cancel_button, visible=True, exception=False) + is not None + ) def wait_for_popup(self): - is_popup_visible = self.browser.wait_for_element( - self.cancel_button, visible=True, exception=False - ) is not None and 'in' in self.browser.classes(self) - are_fields_visible = self.browser.wait_for_element( - self.general.subscribe_to, visible=True, exception=False + return ( + self.browser.wait_for_element( + self.cancel_button, visible=True, timeout=30, exception=False + ) + is not None ) - return is_popup_visible and are_fields_visible -class WebhookEditView(WebhookCreateView): - @property - def is_displayed(self): - return self.browser.wait_for_element( - self.cancel_button, visible=True, exception=False - ) is not None and 'in' in self.browser.classes(self) +class WebhookCreateView(WebhookFormView): + ROOT = '//div[@id="webhookCreateModal"]' + + +class WebhookEditView(WebhookFormView): + ROOT = '//div[@id="webhookEditModal"]' class DeleteWebhookConfirmationView(BaseLoggedInView): - ROOT = ( - '//div[@role="dialog" and @tabindex]' - '[div//h4[normalize-space(.)="Confirm Webhook Deletion"]]' - ) - delete_button = Button('contains', 'Delete') - cancel_button = Button('Cancel') + ROOT = '//div[@id="webhookDeleteModal"]' + delete_button = PF5Button('Delete') + cancel_button = PF5Button('Cancel') @property def is_displayed(self): - return self.browser.wait_for_element( - self.delete_button, visible=True, exception=False - ) is not None and 'in' in self.browser.classes(self) + return ( + self.browser.wait_for_element(self.delete_button, visible=True, exception=False) + is not None + ) def wait_animation_end(self): - wait_for( - lambda: 'in' in self.browser.classes(self), - handle_exception=True, - logger=self.logger, - timeout=10, - ) + self.browser.wait_for_element(self.delete_button, visible=True, timeout=10) diff --git a/airgun/widgets.py b/airgun/widgets.py index 2a9adb822..84745c057 100644 --- a/airgun/widgets.py +++ b/airgun/widgets.py @@ -3123,3 +3123,44 @@ def read(self, expand=True): row_data['children'] = self.get_children(i) result.append(row_data) return result + + +class PF5TypeaheadSelect(Widget): + """Widget for PF5 typeahead select components (type to filter + select). + + These components have an input field where you type to filter options, + then select from a dropdown menu that appears. + + Args: + locator: XPath locator for the input element + """ + + def __init__(self, parent, locator, logger=None): + super().__init__(parent, logger=logger) + self.locator = locator + + def __locator__(self): + return self.locator + + def _get_option_locator(self, value): + """Build an XPath locator for the dropdown menu option.""" + return ( + f'//*[@id="select-typeahead-listbox"]' + f'//button[contains(@class, "pf-v5-c-menu__item") and normalize-space(.)="{value}"]' + ) + + def fill(self, value): + """Type value and click matching option.""" + input_el = self.browser.wait_for_element(self.locator, timeout=30, exception=True) + self.browser.clear(input_el) + input_el.send_keys(value) + + option_locator = self._get_option_locator(value) + option_el = self.browser.wait_for_element(option_locator, timeout=10, exception=True) + option_el.click() + + self.browser.wait_for_element(option_locator, timeout=5, exception=False, visible=False) + + def read(self): + """Read current value from the input field.""" + return self.browser.get_attribute('value', self.browser.element(self.locator)) or ''