diff --git a/src/widgetastic_patternfly5/ouia.py b/src/widgetastic_patternfly5/ouia.py new file mode 100644 index 0000000..f1e9875 --- /dev/null +++ b/src/widgetastic_patternfly5/ouia.py @@ -0,0 +1,112 @@ +from widgetastic.ouia import OUIAGenericView +from widgetastic.ouia import OUIAGenericWidget +from widgetastic.ouia.input import TextInput as BaseOuiaTextInput +from widgetastic.ouia.text import Text as BaseOuiaText +from widgetastic.widget.table import Table +from widgetastic.xpath import quote + +from widgetastic_patternfly5.components.alert import BaseAlert +from widgetastic_patternfly5.components.breadcrumb import BaseBreadCrumb +from widgetastic_patternfly5.components.button import BaseButton +from widgetastic_patternfly5.components.card import BaseCard +from widgetastic_patternfly5.components.forms.formselect import BaseFormSelect +from widgetastic_patternfly5.components.menus.menu import BaseCheckboxMenu +from widgetastic_patternfly5.components.menus.menu import BaseMenu +from widgetastic_patternfly5.components.modal import BaseModal +from widgetastic_patternfly5.components.navigation import BaseNavigation +from widgetastic_patternfly5.components.pagination import BaseCompactPagination +from widgetastic_patternfly5.components.pagination import BasePagination +from widgetastic_patternfly5.components.table import BaseExpandableTable +from widgetastic_patternfly5.components.table import BasePatternflyTable +from widgetastic_patternfly5.components.title import BaseTitle + + +class Alert(BaseAlert, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/Alert" + + +class BreadCrumb(BaseBreadCrumb, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/Breadcrumb" + + +class Button(BaseButton, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/Button" + + +class Card(BaseCard, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/Card" + + +class FormSelect(BaseFormSelect, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/FormSelect" + + +class Menu(BaseMenu): + OUIA_COMPONENT_TYPE = "PF5/Menu" + + +class CheckboxMenu(BaseCheckboxMenu): + OUIA_COMPONENT_TYPE = "PF5/Menu" + + +class Modal(BaseModal, OUIAGenericView): + OUIA_COMPONENT_TYPE = "PF5/ModalContent" + + +class Navigation(BaseNavigation, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/Nav" + + +class Pagination(BasePagination, OUIAGenericView): + OUIA_COMPONENT_TYPE = "PF5/Pagination" + + +class CompactPagination(BaseCompactPagination, Pagination): + pass + + +class PatternflyTable(BasePatternflyTable, Table): + def __init__( + self, + parent, + component_id, + column_widgets=None, + assoc_column=None, + rows_ignore_top=None, + rows_ignore_bottom=None, + top_ignore_fill=False, + bottom_ignore_fill=False, + logger=None, + ): + self.component_type = "PF5/Table" + self.component_id = component_id + super().__init__( + parent, + locator=( + f".//*[@data-ouia-component-type={quote(self.component_type)} " + f"and @data-ouia-component-id={quote(self.component_id)}]" + ), + column_widgets=column_widgets, + assoc_column=assoc_column, + rows_ignore_top=rows_ignore_top, + rows_ignore_bottom=rows_ignore_bottom, + top_ignore_fill=top_ignore_fill, + bottom_ignore_fill=bottom_ignore_fill, + logger=logger, + ) + + +class ExpandableTable(BaseExpandableTable, PatternflyTable): + pass + + +class Title(BaseTitle, OUIAGenericWidget): + OUIA_COMPONENT_TYPE = "PF5/Title" + + +class Text(BaseOuiaText): + OUIA_COMPONENT_TYPE = "PF5/Text" + + +class TextInput(BaseOuiaTextInput): + OUIA_COMPONENT_TYPE = "PF5/TextInput" diff --git a/testing/ouia/test_alert_ouia.py b/testing/ouia/test_alert_ouia.py new file mode 100644 index 0000000..ac71643 --- /dev/null +++ b/testing/ouia/test_alert_ouia.py @@ -0,0 +1,27 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5.ouia import Alert as AlertOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/alert" + +ALERT_TYPES = ["InfoAlert", "SuccessAlert", "WarningAlert", "DangerAlert"] + + +@pytest.fixture(params=ALERT_TYPES) +def alert(browser, request): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-alert-alert-variants']" + alert = AlertOUIA(request.param) + + view = TestView(browser) + return view.alert + + +def test_alert_is_displayed(alert): + assert alert.is_displayed + + +def test_alert_title(alert): + alert_type = alert.type if alert.type != "error" else "danger" + assert alert.title == f"{alert_type.capitalize()} alert title" diff --git a/testing/ouia/test_breadcrumb_ouia.py b/testing/ouia/test_breadcrumb_ouia.py new file mode 100644 index 0000000..6fd20f6 --- /dev/null +++ b/testing/ouia/test_breadcrumb_ouia.py @@ -0,0 +1,42 @@ +import re + +import pytest +from widgetastic.exceptions import WidgetOperationFailed +from widgetastic.widget import View + +from widgetastic_patternfly5.ouia import BreadCrumb as BreadCrumbOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/breadcrumb/" + + +def test_breadcrumb(browser): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-breadcrumb-basic']" + breadcrumb = BreadCrumbOUIA("BasicBreadcrumb") + + view = TestView(browser) + assert view.breadcrumb.is_displayed + assert len(view.breadcrumb.locations) == 4 + assert view.breadcrumb.locations[0].lower() == "section home" + assert view.breadcrumb.read().lower() == "section landing" + view.breadcrumb.click_location(view.breadcrumb.locations[0]) + view.breadcrumb.click_location("title", partial=True) + + failing_location = "definitely not in the example page" + + # exception + message on full match + exception_match = re.escape( + f'Breadcrumb location "{failing_location}" ' + f"not found within locations: {view.breadcrumb.locations}" + ) + with pytest.raises(WidgetOperationFailed, match=exception_match): + view.breadcrumb.click_location(failing_location) + + # exception + message on partial match + exception_match = re.escape( + f'Breadcrumb location "{failing_location}" ' + "not found with partial match " + f"within locations: {view.breadcrumb.locations}" + ) + with pytest.raises(WidgetOperationFailed, match=exception_match): + view.breadcrumb.click_location(failing_location, partial=True) diff --git a/testing/ouia/test_button_ouia.py b/testing/ouia/test_button_ouia.py new file mode 100644 index 0000000..91d12e9 --- /dev/null +++ b/testing/ouia/test_button_ouia.py @@ -0,0 +1,26 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5.ouia import Button as ButtonOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/button" + +BUTTON_TYPES = ["Primary", "Secondary", "DangerSecondary", "Tertiary", "Danger", "Warning"] + + +@pytest.fixture(params=BUTTON_TYPES) +def button(browser, request): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-button-variant-examples']" + button = ButtonOUIA(request.param) + + view = TestView(browser) + return view.button + + +def test_button_is_displayed(button): + assert button.is_displayed + + +def test_button_is_active(button): + assert not button.disabled diff --git a/testing/ouia/test_card_ouia.py b/testing/ouia/test_card_ouia.py new file mode 100644 index 0000000..121900b --- /dev/null +++ b/testing/ouia/test_card_ouia.py @@ -0,0 +1,19 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5.ouia import Card as CardOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/card" + + +@pytest.fixture +def view(browser): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-card-basic-cards']" + card = CardOUIA("BasicCard") + + return TestView(browser) + + +def test_card_displayed(view): + assert view.card.is_displayed diff --git a/testing/ouia/test_formselector_ouia.py b/testing/ouia/test_formselector_ouia.py new file mode 100644 index 0000000..219185f --- /dev/null +++ b/testing/ouia/test_formselector_ouia.py @@ -0,0 +1,48 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5 import FormSelectOptionNotFound +from widgetastic_patternfly5.ouia import FormSelect as FormSelectOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/forms/form-select" + + +@pytest.fixture +def view(browser): + class FormSelectTestView(View): + ROOT = ".//div[@id='ws-react-c-form-select-basic']" + input = FormSelectOUIA("BasicFormSelect") + + return FormSelectTestView(browser) + + +def test_formselect_visibility(view): + assert view.input.is_displayed + + +def test_formselect_enablement(view): + assert view.input.is_enabled + + +def test_formselect_validity(view): + assert view.input.is_valid + + +def test_formselect_value(view): + assert len(view.input.all_options) == 7 + assert len(view.input.all_enabled_options) == 6 + assert "Mrs" in view.input.all_options + view.input.fill("Mrs") + assert view.input.read() == "Mrs" + + +def test_formselect_option_enablement(view): + expected_enabled_options = {"Mr", "Miss", "Mrs", "Ms", "Dr", "Other"} + expected_disabled_options = {"Please Choose"} + assert set(view.input.all_enabled_options) == expected_enabled_options + assert expected_disabled_options not in set(view.input.all_enabled_options) + + +def test_formselect_fill_nonexistent_option(view): + with pytest.raises(FormSelectOptionNotFound): + view.input.fill("foo") diff --git a/testing/ouia/test_modal_ouia.py b/testing/ouia/test_modal_ouia.py new file mode 100644 index 0000000..52b0d13 --- /dev/null +++ b/testing/ouia/test_modal_ouia.py @@ -0,0 +1,71 @@ +import pytest +from widgetastic.widget import Text +from widgetastic.widget import View + +from widgetastic_patternfly5 import ModalItemNotFound +from widgetastic_patternfly5.ouia import Button as ButtonOUIA +from widgetastic_patternfly5.ouia import Modal as ModalOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/modal" + + +@pytest.fixture() +def modal(browser): + class ModalTestView(View): + ROOT = ".//div[@id='ws-react-c-modal-basic-modals']" + show_modal = ButtonOUIA("ShowBasicModal") + + modal = ModalOUIA(browser, "BasicModal") + + view = ModalTestView(browser) + view.show_modal.click() + yield modal + if modal.is_displayed: + modal.close() + + +class CustomModal(ModalOUIA): + """Model use as view and enhance with widgets""" + + custom_body = Text(".//div[contains(@class, 'pf-v5-c-modal-box__body')]") + + +def test_title(modal): + assert modal.title + + +def test_body(modal): + body = modal.body + assert body.text.startswith("Lorem") + + +def test_close(modal): + modal.close() + assert not modal.is_displayed + + +def test_footer_items(modal): + items = modal.footer_items + assert len(items) == 2 + assert "Cancel" in items + assert "Confirm" in items + + +def test_footer_item(modal): + item = modal.footer_item("Confirm") + assert item.text == "Confirm" + item.click() + assert not modal.is_displayed + + +def test_footer_item_invalid(modal): + items = modal.footer_items + with pytest.raises(ModalItemNotFound) as e: + modal.footer_item("INVALID") + assert str(e.value) == f"Item INVALID not found. Available items: {items}" + + +def test_modal_as_view(browser, modal): + view = CustomModal(browser, "BasicModal") + assert view.is_displayed + assert view.custom_body.text == modal.body.text diff --git a/testing/ouia/test_navigation_ouia.py b/testing/ouia/test_navigation_ouia.py new file mode 100644 index 0000000..ac56510 --- /dev/null +++ b/testing/ouia/test_navigation_ouia.py @@ -0,0 +1,25 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5.ouia import Navigation as NavigationOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/navigation" + + +@pytest.fixture +def view(browser): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-navigation-default']" + nav = NavigationOUIA("DefaultNav") + + return TestView(browser) + + +def test_navigation(browser, view): + assert view.nav.currently_selected == ["Default Link 1"] + assert view.nav.nav_item_tree() == [ + "Default Link 1", + "Default Link 2", + "Default Link 3", + "Default Link 4", + ] diff --git a/testing/ouia/test_pagination_ouia.py b/testing/ouia/test_pagination_ouia.py new file mode 100644 index 0000000..6545774 --- /dev/null +++ b/testing/ouia/test_pagination_ouia.py @@ -0,0 +1,122 @@ +import pytest +from wait_for import wait_for +from widgetastic.widget import View + +from widgetastic_patternfly5 import PaginationNavDisabled +from widgetastic_patternfly5.ouia import Pagination as PaginationOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/pagination" + + +@pytest.fixture +def paginator(browser): + class TestView(View): + ROOT = ".//div[@id='ws-react-c-pagination-top']" + paginator = PaginationOUIA("PaginationTop") + + paginator = TestView(browser).paginator + wait_for(lambda: paginator.is_displayed, num_sec=10) + yield paginator + try: + paginator.first_page() + except PaginationNavDisabled: + # We are already at the first page... + pass + + +def test_first_page(paginator): + paginator.last_page() + assert paginator.current_page == 27 + paginator.first_page() + assert paginator.is_first_disabled + assert paginator.is_previous_disabled + assert not paginator.is_next_disabled + assert not paginator.is_last_disabled + assert paginator.current_page == 1 + assert paginator.total_pages == 27 + assert paginator.displayed_items == (1, 20) + assert paginator.total_items == 523 + + +def test_previous_page(paginator): + paginator.next_page() + assert paginator.current_page == 2 + paginator.previous_page() + assert not paginator.is_next_disabled + assert not paginator.is_last_disabled + assert paginator.current_page == 1 + assert paginator.total_pages == 27 + assert paginator.displayed_items == (1, 20) + assert paginator.total_items == 523 + + +def test_next_page(paginator): + paginator.next_page() + assert not paginator.is_first_disabled + assert not paginator.is_previous_disabled + assert not paginator.is_next_disabled + assert not paginator.is_last_disabled + assert paginator.current_page == 2 + assert paginator.total_pages == 27 + assert paginator.displayed_items == (21, 40) + assert paginator.total_items == 523 + + +def test_last_page(paginator): + paginator.last_page() + assert not paginator.is_first_disabled + assert not paginator.is_previous_disabled + assert paginator.is_next_disabled + assert paginator.is_last_disabled + assert paginator.current_page == 27 + assert paginator.total_pages == 27 + assert paginator.displayed_items == (521, 523) + assert paginator.total_items == 523 + + +def test_per_page_options(paginator): + assert paginator.per_page_options == [ + "10 per page", + "20 per page", + "50 per page", + "100 per page", + ] + + +@pytest.mark.parametrize("items_per_page", [50, 100]) +def test_iteration(paginator, items_per_page): + assert paginator.is_first_disabled + paginator.set_per_page(items_per_page) + assert paginator.current_per_page == items_per_page + + # Ensure we're always using an int for the math calculations + items_per_page_int = int(str(items_per_page).split()[0]) + + expected_total_pages = 523 // items_per_page_int + 1 + assert paginator.is_previous_disabled + with paginator.cache_per_page_value(): + for page in paginator: + assert paginator.current_page == page + assert paginator.total_pages == expected_total_pages + if items_per_page_int * page > paginator.total_items: + right_number = paginator.total_items + else: + right_number = items_per_page_int * page + assert paginator.displayed_items == (1 + items_per_page_int * (page - 1), right_number) + assert paginator.total_items == 523 + assert paginator.is_next_disabled + assert paginator.is_last_disabled + + +def test_bad_paginator_page_value(paginator): + with pytest.raises(ValueError): + paginator.set_per_page(9999999) + with pytest.raises(ValueError): + paginator.set_per_page("999999") + + +def test_custom_page(paginator): + disp_items = paginator.displayed_items + paginator.go_to_page(2) + assert paginator.current_page == 2 + assert disp_items != paginator.displayed_items diff --git a/testing/ouia/test_table_ouia.py b/testing/ouia/test_table_ouia.py new file mode 100644 index 0000000..f84e392 --- /dev/null +++ b/testing/ouia/test_table_ouia.py @@ -0,0 +1,27 @@ +import pytest +from widgetastic.widget import View + +from widgetastic_patternfly5.ouia import PatternflyTable as PatternflyTableOUIA + +TESTING_PAGE_URL = "https://patternfly-react-main.surge.sh/components/table" + +SORT = [ + ("Repositories table header that goes on for a long time.", "ascending", ["a", "one", "p"]), + ("Repositories table header that goes on for a long time.", "descending", ["p", "one", "a"]), + ("Pull requests table header that goes on for a long time.", "ascending", ["a", "b", "k"]), + ("Pull requests table header that goes on for a long time.", "descending", ["k", "b", "a"]), +] + + +@pytest.mark.parametrize("sample", SORT, ids=lambda sample: "{}-{}".format(sample[0], sample[1])) +def test_sortable_table(browser, sample): + header, order, expected_result = sample + + class TestView(View): + ROOT = ".//div[contains(@id, 'ws-react-c-table-sortable')]" + table = PatternflyTableOUIA("SortableTable") + + view = TestView(browser) + view.table.sort_by(header, order) + column = [row[header] for row in view.table.read()] + assert column == expected_result