From 3f7fd2d40289a6ca7d5340975082ef066cdaf9ed Mon Sep 17 00:00:00 2001 From: Mike Shriver Date: Tue, 25 Oct 2022 09:22:18 -0400 Subject: [PATCH] Add radio widget PF4 has no radio group, just radio provide unit tests as well Radio has description/label/checkbox widgets that should be consistent in practice body will have to be replaced within a Radio subclass when used in practice on radios that have additional widgets in their body Filling a radio just clicks the checkbox, Radio.body will have to be filled separately from the selection itself. child_widget_accessed could be used in the body definition, maybe there's a better strategy to define the body widgets? --- src/widgetastic_patternfly4/__init__.py | 22 +++++---- src/widgetastic_patternfly4/radio.py | 61 +++++++++++++++++++++++++ testing/test_radio.py | 53 +++++++++++++++++++++ 3 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 src/widgetastic_patternfly4/radio.py create mode 100644 testing/test_radio.py diff --git a/src/widgetastic_patternfly4/__init__.py b/src/widgetastic_patternfly4/__init__.py index faa73253..6be0e20e 100644 --- a/src/widgetastic_patternfly4/__init__.py +++ b/src/widgetastic_patternfly4/__init__.py @@ -41,6 +41,7 @@ from .piechart import PieChart from .popover import Popover from .progress import Progress +from .radio import Radio from .select import CheckboxSelect from .select import Select from .select import SelectItemDisabled @@ -69,29 +70,28 @@ "ChipGroup", "ChipGroupToolbar", "ChipGroupToolbarCategory", - "StandAloneChipGroup", "Card", "CardGroup", "CardForCardGroup", "CardCheckBox", + "CategoryChipGroup", "ColumnNotExpandable", "CompactPagination", "CompoundExpandableTable", - "CategoryChipGroup", + "ContextSelector", "DonutChart", "Dropdown", "DropdownDisabled", "DropdownItemDisabled", "DropdownItemNotFound", "DualListSelector", - "SearchDualListSelector", - "GroupDropdown", - "InputSlider", - "SplitButtonDropdown", + "ExpandableTable", "FormSelect", "FormSelectDisabled", "FormSelectOptionDisabled", "FormSelectOptionNotFound", + "GroupDropdown", + "InputSlider", "Menu", "MenuItemDisabled", "MenuItemNotFound", @@ -101,19 +101,21 @@ "OptionsMenu", "Pagination", "PaginationNavDisabled", + "PatternflyTable", "PieChart", "Popover", "Progress", + "Radio", + "RowNotExpandable", + "SearchDualListSelector", "Select", "SelectItemDisabled", "SelectItemNotFound", - "ContextSelector", "Slider", + "SplitButtonDropdown", + "StandAloneChipGroup", "Switch", "SwitchDisabled", - "ExpandableTable", - "PatternflyTable", - "RowNotExpandable", "Tab", "Title", ] diff --git a/src/widgetastic_patternfly4/radio.py b/src/widgetastic_patternfly4/radio.py new file mode 100644 index 00000000..41556c42 --- /dev/null +++ b/src/widgetastic_patternfly4/radio.py @@ -0,0 +1,61 @@ +from widgetastic.widget import Checkbox +from widgetastic.widget import ParametrizedLocator +from widgetastic.widget import Text +from widgetastic.widget import View + +# https://patternfly-react.surge.sh/components/radio + + +class BaseRadio: + ROOT_ID_LOC = ParametrizedLocator( + ".//div[contains(@class, 'pf-c-radio') and " ".//input[@type='radio' and @id={@id|quote}]]" + ) + ROOT_LABEL_LOC = ParametrizedLocator( + ".//div[contains(@class, 'pf-c-radio') and " + ".//*[contains(@class, 'pf-c-radio__label') and normalize-space(.)={@label_text|quote}]]" + ) + DESC_LOC = ".//*[contains(@class, 'pf-c-radio__description')]" + BODY_LOC = ".//*[contains(@class, 'pf-c-radio__body')]" + RADIO_LOC = ".//input[contains(@class, 'pf-c-radio__input')]" + LABEL_LOC = ".//*[contains(@class, 'pf-c-radio__label')]" + + def __init__(self, parent, id=None, label_text=None, **kwargs): + """Generate locator based on either id or label (but not both)""" + super().__init__(parent, **kwargs) + if id is not None and label_text is not None: + raise TypeError("Cannot create Radio with id and label set") + self.id = id + self.label_text = label_text + self.locator = self.ROOT_ID_LOC if id is not None else self.ROOT_LABEL_LOC + + @property + def body(self): + """Consider nesting a view when subclassing to override and add widgets""" + return self.browser.element(self.BODY_LOC) + + +class Radio(BaseRadio, View): + """Base Radio view, subclass to add widgets to the body""" + + ROOT = ParametrizedLocator("{@locator}") + + description = Text(BaseRadio.DESC_LOC) + radio = Checkbox(locator=BaseRadio.RADIO_LOC) + label = Text(BaseRadio.LABEL_LOC) + body = Text(BaseRadio.BODY_LOC) # subclass + View.nested class to inject body with more widgets + + @property + def selected(self): + return self.radio.selected + + @property + def disabled(self): + return "pf-m-disabled" in self.browser.classes(self.label) + + def fill(self, values): + """Can only handle `True` to check the radio, nature of individual radio button""" + return self.radio.fill(values) + + +# TODO there is a 'name' attribute used by PF for correlating radio buttons +# This could be used to create a `RadioGroup` class of Radio widgets diff --git a/testing/test_radio.py b/testing/test_radio.py new file mode 100644 index 00000000..30f61f1f --- /dev/null +++ b/testing/test_radio.py @@ -0,0 +1,53 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly4 import Radio + + +TESTING_PAGE_URL = "https://patternfly-react.surge.sh/components/radio" + + +class RadioTestView(View): + controlled_id = Radio(id="radio-controlled") + uncontrolled_label = Radio(label_text="Uncontrolled radio example") + description_body = Radio(id="radio-description-body") + disabled_radio = Radio(id="radio-disabled") + disabled_checked = Radio(id="radio-disabled-checked") + + +@pytest.mark.parametrize( + "test_widget", + [ + ("controlled_id", dict(radio=False, label="Controlled radio")), + ("uncontrolled_label", dict(radio=False, label="Uncontrolled radio example")), + ( + "description_body", + dict( + radio=False, + label="Radio with description and body", + description="Single-tenant cloud service hosted and managed by Red Hat that offers " + "high-availability enterprise-grade clusters in a virtual private cloud on " + "AWS or GCP.", + body="This is where custom content goes.", + ), + ), + ], + ids=["id_locator", "label_locator", "with_description"], +) +def test_location(browser, test_widget): + widget_name, expected_read = test_widget + view = RadioTestView(browser) + widget = getattr(view, widget_name) + assert widget.is_displayed + assert widget.read() == expected_read + assert widget.selected is False + assert widget.fill(True) is True + assert widget.selected is True + + +def test_disabled(browser): + view = RadioTestView(browser) + assert view.disabled_radio.disabled is True + assert view.disabled_radio.selected is False + assert view.disabled_checked.disabled is True + assert view.disabled_checked.selected is True