Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions airgun/entities/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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()
174 changes: 96 additions & 78 deletions airgun/views/webhook.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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")]'),
},
)

Expand All @@ -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)
41 changes: 41 additions & 0 deletions airgun/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ''