From 1b493c1ec32a64cbff20d3cfe7f43afab1e01892 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Thu, 21 Aug 2025 11:41:26 -0700 Subject: [PATCH 1/5] Handle nav panel clicks inside dropdown menus Update NavPanel.click to first open the dropdown if the nav panel is inside a closed dropdown menu. This ensures the nav panel is interactable before attempting to click it. --- shiny/playwright/controller/_navs.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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: From 873119e9a9c10c0b07d11df44c2ca58f818b65e6 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Thu, 21 Aug 2025 14:36:53 -0700 Subject: [PATCH 2/5] Add tests for navset_menu component in Shiny Express Introduces an example Shiny Express app using navset_pill and navset_underline with nav_menu, and adds Playwright-based tests to verify navigation between panels. This enhances test coverage for navigation UI components. --- .../components/nav/navset_menu/app-express.py | 52 +++++++++++++++++++ .../components/nav/navset_menu/test_app.py | 37 +++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tests/playwright/shiny/components/nav/navset_menu/app-express.py create mode 100644 tests/playwright/shiny/components/nav/navset_menu/test_app.py 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..043b68bae --- /dev/null +++ b/tests/playwright/shiny/components/nav/navset_menu/test_app.py @@ -0,0 +1,37 @@ +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: type[controller.NavsetPill] | type[controller.NavsetUnderline], + nav_id: str, + out_id: str, +): + """Test navigation between panels for different navset types.""" + 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) From 413c60b9c996fd2848032b845fcaa55fbf9ad440 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Thu, 21 Aug 2025 14:43:20 -0700 Subject: [PATCH 3/5] Update changelog with Navset menu item selection support Documented the addition of support for selecting menu items in Navset controllers to improve dropdown navigation test coverage. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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) From 1c47a5576e3aab22c8c91b419d4feffdb2baeb33 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Thu, 21 Aug 2025 14:54:57 -0700 Subject: [PATCH 4/5] Use Union for nav_factory type annotation Replaces the use of the | operator with Union in the type annotation for nav_factory in test_navset_menu for improved compatibility and clarity. --- tests/playwright/shiny/components/nav/navset_menu/test_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/playwright/shiny/components/nav/navset_menu/test_app.py b/tests/playwright/shiny/components/nav/navset_menu/test_app.py index 043b68bae..83d6f9931 100644 --- a/tests/playwright/shiny/components/nav/navset_menu/test_app.py +++ b/tests/playwright/shiny/components/nav/navset_menu/test_app.py @@ -1,5 +1,6 @@ import pytest from playwright.sync_api import Page +from typing import Union from shiny.playwright import controller from shiny.pytest import create_app_fixture @@ -19,7 +20,7 @@ def test_navset_menu( page: Page, app: ShinyAppProc, - nav_factory: type[controller.NavsetPill] | type[controller.NavsetUnderline], + nav_factory: Union[type[controller.NavsetPill], type[controller.NavsetUnderline]], nav_id: str, out_id: str, ): From f0886bee0d1f35678a5391d23878dd14f7ee49ab Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Thu, 21 Aug 2025 14:56:14 -0700 Subject: [PATCH 5/5] Reorder and clean up imports in test_app.py Moved 'from typing import Union' to the top and removed a redundant import statement. Also removed an unnecessary docstring in the test function. --- .../playwright/shiny/components/nav/navset_menu/test_app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/playwright/shiny/components/nav/navset_menu/test_app.py b/tests/playwright/shiny/components/nav/navset_menu/test_app.py index 83d6f9931..028374707 100644 --- a/tests/playwright/shiny/components/nav/navset_menu/test_app.py +++ b/tests/playwright/shiny/components/nav/navset_menu/test_app.py @@ -1,6 +1,7 @@ +from typing import Union + import pytest from playwright.sync_api import Page -from typing import Union from shiny.playwright import controller from shiny.pytest import create_app_fixture @@ -24,7 +25,7 @@ def test_navset_menu( nav_id: str, out_id: str, ): - """Test navigation between panels for different navset types.""" + page.goto(app.url) navset = nav_factory(page, nav_id) output: controller.OutputText = controller.OutputText(page, out_id)