Skip to content

Commit 910225a

Browse files
authored
Add views and entities for SAT-21596 (#2465)
1 parent a028eda commit 910225a

3 files changed

Lines changed: 129 additions & 28 deletions

File tree

airgun/entities/contentcredential.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from wait_for import wait_for
22

33
from airgun.entities.base import BaseEntity
4+
from airgun.exceptions import DisabledWidgetError
45
from airgun.navigation import NavigateStepWithWait as NavigateStep, navigator
6+
from airgun.views.common import TableRowKebabMenu
57
from airgun.views.contentcredential import (
6-
ContentCredentialCreateView,
78
ContentCredentialEditView,
89
ContentCredentialsTableView,
10+
CreateContentCredentialModal,
911
DeleteContentCredentialModal,
1012
)
1113
from airgun.views.product import ProductEditView
@@ -16,15 +18,60 @@ class ContentCredentialEntity(BaseEntity):
1618
endpoint_path = '/labs/content_credentials'
1719

1820
def create(self, values):
19-
"""Create new content credentials entity."""
20-
raise NotImplementedError(
21-
'Content Credential create is not yet implemented in the React UI'
21+
"""Create new content credentials entity via modal.
22+
23+
Args:
24+
values (dict): Should contain:
25+
- name (str): Name of the credential
26+
- content_type (str): Type - 'GPG Key' or 'Certificate'
27+
- content (str): The GPG key or certificate content
28+
29+
Raises:
30+
DisabledWidgetError: If the create button is disabled due to missing required fields
31+
"""
32+
view = self.navigate_to(self, 'All')
33+
view.create_button.click()
34+
modal = CreateContentCredentialModal(self.browser)
35+
modal.wait_displayed()
36+
modal.fill(
37+
{
38+
'name_input': values.get('name', ''),
39+
'content_type': values.get('content_type', ''),
40+
'content_text_box': values.get('content', ''),
41+
}
42+
)
43+
if modal.create_button.disabled:
44+
raise DisabledWidgetError(
45+
'Create button is disabled. Required fields (name and content) must be filled.'
46+
)
47+
modal.create_button.click()
48+
wait_for(
49+
lambda: not modal.is_displayed,
50+
timeout=30,
51+
delay=0.5,
52+
handle_exception=True,
53+
message='Waiting for create modal to close',
54+
)
55+
wait_for(
56+
lambda: view.table.row(name=values['name']),
57+
timeout=30,
58+
delay=0.5,
59+
handle_exception=True,
60+
message=f'Waiting for credential "{values["name"]}" to appear in table',
2261
)
2362

2463
def delete(self, entity_name):
25-
"""Delete existing content credentials entity via kebab menu."""
26-
view = self.navigate_to(self, 'Edit', entity_name=entity_name)
27-
view.actions.item_select('Delete')
64+
"""Delete existing content credentials entity via table row kebab menu."""
65+
view = self.navigate_to(self, 'All')
66+
view.search(entity_name)
67+
wait_for(
68+
lambda: view.table.row(name=entity_name),
69+
timeout=30,
70+
delay=0.5,
71+
handle_exception=True,
72+
message=f'Waiting for credential "{entity_name}" row to appear',
73+
)
74+
TableRowKebabMenu(parent=view.table.row(name=entity_name)).item_select('Delete')
2875
modal = DeleteContentCredentialModal(self.browser)
2976
modal.wait_displayed()
3077
modal.confirm_delete.click()
@@ -108,18 +155,6 @@ def step(self, *args, **kwargs):
108155
self.view.menu.select('Lab Features', 'Content Credentials')
109156

110157

111-
@navigator.register(ContentCredentialEntity, 'New')
112-
class AddNewContentCredential(NavigateStep):
113-
"""Navigate to Create Content Credential page (stub)."""
114-
115-
VIEW = ContentCredentialCreateView
116-
117-
def step(self, *args, **kwargs):
118-
raise NotImplementedError(
119-
'Content Credential create is not yet implemented in the React UI'
120-
)
121-
122-
123158
@navigator.register(ContentCredentialEntity, 'Edit')
124159
class EditContentCredential(NavigateStep):
125160
"""Navigate to Content Credential details screen.

airgun/views/contentcredential.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from widgetastic.utils import ParametrizedLocator
2-
from widgetastic.widget import Text, View
2+
from widgetastic.widget import Text, TextInput, View
33
from widgetastic_patternfly import Tab
4+
from widgetastic_patternfly5.components.forms.form_select import FormSelect
45
from widgetastic_patternfly5.ouia import (
56
BreadCrumb as PF5OUIABreadCrumb,
67
Button as PF5OUIAButton,
@@ -11,13 +12,14 @@
1112
)
1213

1314
from airgun.views.common import BaseLoggedInView, SearchableViewMixinPF4
14-
from airgun.widgets import PF5SpacedListItem
15+
from airgun.widgets import PF5EditableSpacedListItem, PF5SpacedListItem
1516

1617

1718
class ContentCredentialsTableView(BaseLoggedInView, SearchableViewMixinPF4):
1819
"""PF5 list page for Content Credentials at /labs/content_credentials."""
1920

2021
title = Text('//h1[normalize-space(.)="Content Credentials"]')
22+
create_button = PF5OUIAButton('action-buttons-create')
2123
table = PF5OUIATable(
2224
component_id='content-credentials-table',
2325
column_widgets={
@@ -27,21 +29,34 @@ class ContentCredentialsTableView(BaseLoggedInView, SearchableViewMixinPF4):
2729

2830
@property
2931
def is_displayed(self):
30-
return self.title.is_displayed and self.table.is_displayed
32+
return (
33+
self.title.is_displayed and self.table.is_displayed and self.create_button.is_displayed
34+
)
35+
36+
37+
class CreateContentCredentialModal(PF5OUIAModal):
38+
"""PF5 confirmation modal for creating a Content Credential."""
3139

40+
OUIA_ID = 'create-content-credential-modal'
3241

33-
class ContentCredentialCreateView(BaseLoggedInView):
34-
# Not yet implemented in React UI
35-
pass
42+
name_input = PF5OUIATextInput('create-content-credential-name-input')
43+
content_type = FormSelect(locator='.//select[@id="content_type"]')
44+
content_text_box = TextInput(locator='.//textarea[@aria-label="content"]')
45+
create_button = PF5OUIAButton('create-content-credential-create-button')
46+
cancel_button = PF5OUIAButton('create-content-credential-cancel-button')
47+
48+
@property
49+
def is_displayed(self):
50+
return self.name_input.is_displayed
3651

3752

3853
class DeleteContentCredentialModal(PF5OUIAModal):
3954
"""PF5 confirmation modal for deleting a Content Credential."""
4055

4156
OUIA_ID = 'delete-content-credential-modal'
4257

43-
confirm_delete = PF5OUIAButton('delete-confirm-button')
44-
cancel = PF5OUIAButton('delete-cancel-button')
58+
confirm_delete = PF5OUIAButton('delete-content-credential-delete-button')
59+
cancel = PF5OUIAButton('delete-content-credential-cancel-button')
4560

4661
@property
4762
def is_displayed(self):
@@ -66,7 +81,7 @@ def is_displayed(self):
6681
class details(Tab):
6782
TAB_LOCATOR = ParametrizedLocator('//*[@data-ouia-component-id="routed-tabs-tab-details"]')
6883

69-
name = PF5SpacedListItem(label='Name')
84+
name = PF5EditableSpacedListItem(label='Name')
7085
content_type = PF5SpacedListItem(label='Type')
7186
content = PF5SpacedListItem(label='Content')
7287
upload_file = PF5OUIAButton('upload-file-button')

airgun/widgets.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,3 +3288,54 @@ def __locator__(self):
32883288
def read(self):
32893289
"""Return the text content of the ``<dd>`` element."""
32903290
return self.browser.text(self).strip()
3291+
3292+
3293+
class PF5EditableSpacedListItem(PF5SpacedListItem):
3294+
"""An editable field in a PF5 description list with inline editing.
3295+
3296+
Extends PF5SpacedListItem to support editing via an edit button, text input, and submit button.
3297+
3298+
Usage::
3299+
3300+
name = PF5EditableSpacedListItem(label='Name')
3301+
"""
3302+
3303+
def fill(self, value):
3304+
"""Edit the field value.
3305+
3306+
Args:
3307+
value: The new value to set
3308+
3309+
Returns:
3310+
bool: True if the value was changed, False otherwise
3311+
"""
3312+
current_value = self.read()
3313+
if current_value == value:
3314+
return False
3315+
3316+
# Click the edit button for this field
3317+
# The edit button has OUIA component-id like 'edit-button-name' where 'name' is the lowercase label
3318+
edit_button_id = f'edit-button-{self.label.lower()}'
3319+
edit_button = PF5OUIAButton(self, component_id=edit_button_id)
3320+
edit_button.click()
3321+
3322+
# Wait for the text input to appear and fill it
3323+
# The input field has an OUIA component-id, but it's auto-generated
3324+
# We'll locate it by being within the same dd element
3325+
text_input_locator = './/input[@type="text"]'
3326+
text_input = TextInput(self, locator=text_input_locator)
3327+
text_input.fill(value)
3328+
3329+
# Click the submit button
3330+
submit_button_id = f'submit-button-{self.label.lower()}'
3331+
submit_button = PF5OUIAButton(self, component_id=submit_button_id)
3332+
submit_button.click()
3333+
3334+
# Wait for edit mode to close (edit button reappears)
3335+
wait_for(
3336+
lambda: edit_button.is_displayed,
3337+
timeout=10,
3338+
delay=0.5,
3339+
)
3340+
3341+
return True

0 commit comments

Comments
 (0)