diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2b3c3d0..1ea86c27c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +* Add support for selecting menu items in `Navset` controllers to improve dropdown navigation test coverage. (#2066) + * `input_date()`, `input_date_range()`, `update_date()`, and `update_date_range()` now supports `""` for values, mins, and maxes. In this case, no date will be specified on the client. (#1713) (#1689) * Restricted the allowable types of the `choices` parameter of `input_select()`, `input_selectize()`, `update_select()`, and `update_selectize()` to actual set of allowable types (previously, the type was suggesting HTML-like values were supported). (#2048) diff --git a/shiny/playwright/controller/_navs.py b/shiny/playwright/controller/_navs.py index 8db173d50..7a5630406 100644 --- a/shiny/playwright/controller/_navs.py +++ b/shiny/playwright/controller/_navs.py @@ -145,11 +145,35 @@ def click(self, *, timeout: Timeout = None) -> None: """ Clicks the nav panel. + If the nav panel is inside a dropdown, playwright will first open the dropdown before selecting the nav panel. + Parameters ---------- timeout The maximum time to wait for the nav panel to be visible and interactable. Defaults to `None`. """ + + parent_ul_loc = self.loc.locator("..").locator("..") + + parent_ul_cls = parent_ul_loc.element_handle().get_attribute("class") + cls_menu_regex = re.compile(rf"(^|\s+){re.escape('dropdown-menu')}(\s+|$)") + cls_show_regex = re.compile(rf"(^|\s+){re.escape('show')}(\s+|$)") + cls_dropdown_regex = re.compile(rf"(^|\s+){re.escape('dropdown')}(\s+|$)") + + # If the item is in a dropdown and the dropdown is closed + if ( + parent_ul_cls + and cls_menu_regex.search(parent_ul_cls) + and not cls_show_regex.search(parent_ul_cls) + ): + grandparent_li_loc = parent_ul_loc.locator("..") + gnd_li_cls = grandparent_li_loc.element_handle().get_attribute("class") + + # Confirm it is a dropdown + if gnd_li_cls and cls_dropdown_regex.search(gnd_li_cls): + # click the grandparent list item to open it before clicking the target item + grandparent_li_loc.click() + self.loc.click(timeout=timeout) def expect_active(self, value: bool, *, timeout: Timeout = None) -> None: diff --git a/tests/playwright/shiny/components/nav/navset_menu/app-express.py b/tests/playwright/shiny/components/nav/navset_menu/app-express.py new file mode 100644 index 000000000..d4e3d6c5e --- /dev/null +++ b/tests/playwright/shiny/components/nav/navset_menu/app-express.py @@ -0,0 +1,52 @@ +from shiny.express import input, render, ui + +with ui.navset_pill(id="selected_navset_pill"): + with ui.nav_panel("A"): + "Panel A content" + + with ui.nav_panel("B"): + "Panel B content" + + with ui.nav_panel("C"): + "Panel C content" + + with ui.nav_menu("Other links"): + with ui.nav_panel("D"): + "Page D content" + + "----" + "Description:" + with ui.nav_control(): + ui.a("Shiny", href="https://shiny.posit.co", target="_blank") +ui.h5("Selected:") + + +@render.code +def _(): + return input.selected_navset_pill() + + +with ui.navset_underline(id="selected_navset_underline"): + with ui.nav_panel("A"): + "Panel A content" + + with ui.nav_panel("B"): + "Panel B content" + + with ui.nav_panel("C"): + "Panel C content" + + with ui.nav_menu("Other links"): + with ui.nav_panel("D"): + "Page D content" + + "----" + "Description:" + with ui.nav_control(): + ui.a("Shiny", href="https://shiny.posit.co", target="_blank") +ui.h5("Selected:") + + +@render.code +def _underline(): + return input.selected_navset_underline() diff --git a/tests/playwright/shiny/components/nav/navset_menu/test_app.py b/tests/playwright/shiny/components/nav/navset_menu/test_app.py new file mode 100644 index 000000000..028374707 --- /dev/null +++ b/tests/playwright/shiny/components/nav/navset_menu/test_app.py @@ -0,0 +1,39 @@ +from typing import Union + +import pytest +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.pytest import create_app_fixture +from shiny.run import ShinyAppProc + +app = create_app_fixture(["app-express.py"]) + + +@pytest.mark.parametrize( + "nav_factory,nav_id,out_id", + [ + (controller.NavsetPill, "selected_navset_pill", "_"), + (controller.NavsetUnderline, "selected_navset_underline", "_underline"), + ], + ids=["pill", "underline"], +) +def test_navset_menu( + page: Page, + app: ShinyAppProc, + nav_factory: Union[type[controller.NavsetPill], type[controller.NavsetUnderline]], + nav_id: str, + out_id: str, +): + + page.goto(app.url) + navset = nav_factory(page, nav_id) + output: controller.OutputText = controller.OutputText(page, out_id) + + navset.expect_value("A") + output.expect_value("A") + + for panel in ["B", "C", "D"]: + navset.set(panel) + navset.expect_value(panel) + output.expect_value(panel)