diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c2bf330c..097d9f479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] +### Breaking changes + +* `.expect_inverse()` for Navset controllers in `shiny.playwright.controllers` now requires a `bool` value. To keep behavior the same, use `.expect_inverse(False)`. (#1668) + +* `.expect_layout()` for Navset controllers in `shiny.playwright.controllers` is now renamed to `.expect_fluid()` and requires a `bool` value. To keep behavior the same, use `.expect_fluid(True)` (#1668) + +### New features + +### Other changes + +* Added `PageNavbar` class to the list of `shiny.playwright.controllers` for testing `ui.page_navbar()`. (#1668) + +* Added `.expect_widths()` to `NavsetPillList` in `shiny.playwright.controllers` for testing `ui.navset_pill_list(widths=)`. (#1668) + ### Bug fixes * A few fixes for `ui.Chat()`, including: diff --git a/shiny/playwright/controller/__init__.py b/shiny/playwright/controller/__init__.py index 89b403d2c..494fd27bf 100644 --- a/shiny/playwright/controller/__init__.py +++ b/shiny/playwright/controller/__init__.py @@ -61,6 +61,7 @@ NavsetPillList, NavsetTab, NavsetUnderline, + PageNavbar, ) from ._output import ( OutputCode, @@ -121,4 +122,5 @@ "NavsetUnderline", "DownloadButton", "DownloadLink", + "PageNavbar", ] diff --git a/shiny/playwright/controller/_navs.py b/shiny/playwright/controller/_navs.py index bd45d1df3..a12c163ff 100644 --- a/shiny/playwright/controller/_navs.py +++ b/shiny/playwright/controller/_navs.py @@ -6,9 +6,12 @@ from playwright.sync_api import expect as playwright_expect from typing_extensions import Literal +from shiny.types import ListOrTuple + from .._types import PatternOrStr, Timeout +from ..expect import expect_to_have_class, expect_to_have_style +from ..expect._internal import expect_attribute_to_have_value from ..expect._internal import expect_class_to_have_value as _expect_class_to_have_value -from ..expect._internal import expect_style_to_have_value as _expect_style_to_have_value from ._base import ( InitLocator, UiWithContainer, @@ -79,8 +82,8 @@ def expect_placement( The maximum time to wait for the expectation to pass. Defaults to `None`. """ ex_class = "card-header" if location == "above" else "card-footer" - playwright_expect(self.loc_container.locator("..")).to_have_class( - ex_class, timeout=timeout + expect_to_have_class( + self.loc_container.locator(".."), ex_class, timeout=timeout ) @@ -424,23 +427,73 @@ def __init__(self, page: Page, id: str) -> None: loc="> li.nav-item", ) - def expect_well(self, has_well: bool, *, timeout: Timeout = None) -> None: + def expect_well(self, value: bool, *, timeout: Timeout = None) -> None: """ Expects the navset pill list to have a well. Parameters ---------- - has_well - `True` if the navset pill list is expected to have a well, `False` otherwise. + value + `True` if the navset pill list is expected to be constructed with a well, + `False` otherwise. timeout The maximum time to wait for the expectation to pass. Defaults to `None`. """ - if has_well: - playwright_expect(self.loc_container.locator("..")).to_have_class("well") - else: - playwright_expect(self.loc_container.locator("..")).not_to_have_class( - "well" + _expect_class_to_have_value( + self.loc_container.locator(".."), "well", has_class=value, timeout=timeout + ) + + def expect_widths( + self, value: ListOrTuple[int], *, timeout: Timeout = None + ) -> None: + """ + Expects the navset pill list to have the specified widths. + + Parameters + ---------- + value + The expected widths of the navset pill list. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + widths = tuple(value) + assert len(widths) == 2, "`value=` must be a tuple of two integers" + assert all( + isinstance(width, int) for width in widths + ), "`value=` must be integers" + + loc_row_container = self.loc_container.locator("..").locator("..") + + # Make sure the two children are present + loc_complicated = loc_row_container.locator( + "xpath=.", + has=self.page.locator(f"> div.col-sm-{widths[0]} + div.col-sm-{widths[1]}"), + ) + + # Make sure there are only two children present + try: + playwright_expect(loc_complicated.locator("> div")).to_have_count( + 2, timeout=timeout ) + except AssertionError as e: + # Make sure there are only two children + playwright_expect(loc_row_container.locator("> div")).to_have_count( + 2, timeout=1 + ) + + expect_to_have_class( + loc_row_container.locator("> div").first, + f"col-sm-{widths[0]}", + timeout=1, + ) + expect_to_have_class( + loc_row_container.locator("> div").last, + f"col-sm-{widths[1]}", + timeout=1, + ) + + # Re-raise the original exception if nothing could be debugged + raise e class _NavsetCardBase( @@ -563,12 +616,12 @@ def __init__(self, page: Page, id: str) -> None: ) -class NavsetBar( +class _NavsetBarBase( _ExpectNavsetSidebarM, _ExpectNavsetTitleM, _NavsetBase, ): - """Controller for :func:`shiny.ui.navset_bar`.""" + """Mixin class for common expectations of nav bars.""" def __init__(self, page: Page, id: str) -> None: """ @@ -627,24 +680,32 @@ def expect_position( timeout=timeout, ) else: - playwright_expect(self._loc_navbar).to_have_class( - re.compile(rf"{position}"), timeout=timeout - ) + expect_to_have_class(self._loc_navbar, position, timeout=timeout) - def expect_inverse(self, *, timeout: Timeout = None) -> None: + def expect_inverse( + self, + value: bool, + *, + timeout: Timeout = None, + ) -> None: """ Expects the navset bar to be light text color if inverse is True Parameters ---------- + value + `True` if the navset bar is expected to have inverse text color, `False` otherwise. timeout The maximum time to wait for the expectation to pass. Defaults to `None`. """ - playwright_expect(self._loc_navbar).to_have_class( - re.compile("navbar-inverse"), timeout=timeout + _expect_class_to_have_value( + self._loc_navbar, + "navbar-inverse", + has_class=value, + timeout=timeout, ) - def expect_bg(self, bg: str, *, timeout: Timeout = None) -> None: + def expect_bg(self, bg: PatternOrStr, *, timeout: Timeout = None) -> None: """ Expects the navset bar to have the specified background color. @@ -655,11 +716,14 @@ def expect_bg(self, bg: str, *, timeout: Timeout = None) -> None: timeout The maximum time to wait for the expectation to pass. Defaults to `None`. """ - _expect_style_to_have_value( - self._loc_navbar, "background-color", f"{bg} !important", timeout=timeout + expect_to_have_style( + self._loc_navbar, + "background-color", + f"{bg} !important", + timeout=timeout, ) - def expect_gap(self, gap: str, *, timeout: Timeout = None) -> None: + def expect_gap(self, gap: PatternOrStr, *, timeout: Timeout = None) -> None: """ Expects the navset bar to have the specified gap. @@ -670,28 +734,128 @@ def expect_gap(self, gap: str, *, timeout: Timeout = None) -> None: timeout The maximum time to wait for the expectation to pass. Defaults to `None`. """ - _expect_style_to_have_value( - self.get_loc_active_content(), "gap", gap, timeout=timeout + expect_to_have_style( + self.get_loc_active_content(), + "gap", + gap, + timeout=timeout, ) - def expect_layout( - self, layout: Literal["fluid", "fixed"] = "fluid", *, timeout: Timeout = None + def expect_fluid( + self, + value: bool, + *, + timeout: Timeout = None, ) -> None: """ - Expects the navset bar to have the specified layout. + Expects the navset bar to have a fluid or fixed layout. Parameters ---------- - layout - The expected layout. + value + `True` if the layout is `fluid` or `False` if it is `fixed`. timeout The maximum time to wait for the expectation to pass. Defaults to `None`. """ - if layout == "fluid": - playwright_expect( - self.loc_container.locator("..").locator("..") - ).to_have_class(re.compile("container-fluid"), timeout=timeout) + if value: + expect_to_have_class( + self._loc_navbar.locator("> div"), + "container-fluid", + timeout=timeout, + ) else: - playwright_expect(self.loc_container.locator("..")).to_have_class( - re.compile("container"), timeout=timeout + expect_to_have_class( + self._loc_navbar.locator("> div"), + "container", + timeout=timeout, ) + + +class NavsetBar(_NavsetBarBase): + """Controller for :func:`shiny.ui.navset_bar`.""" + + +class PageNavbar(_NavsetBarBase): + """Controller for :func:`shiny.ui.page_navbar`.""" + + def expect_fillable(self, value: bool, *, timeout: Timeout = None) -> None: + """ + Expects the main content area to be considered a fillable (i.e., flexbox) container + + Parameters + ---------- + value + `True` if the main content area is expected to be fillable, `False` otherwise. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + # confirm page is fillable + _expect_class_to_have_value( + self.page.locator("body"), + "bslib-page-fill", + has_class=value, + timeout=timeout, + ) + + # confirm content is fillable + _expect_class_to_have_value( + self.get_loc_active_content(), + "html-fill-container", + has_class=value, + timeout=timeout, + ) + + def expect_fillable_mobile(self, value: bool, *, timeout: Timeout = None) -> None: + """ + Expects the main content area to be considered a fillable (i.e., flexbox) container on mobile + This method will always call `.expect_fillable(True)` first to ensure the fillable property is set + + Parameters + ---------- + value + `True` if the main content area is expected to be fillable on mobile, `False` otherwise. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + + # This is important since fillable_mobile needs fillable property to be True + self.expect_fillable(True, timeout=timeout) + _expect_class_to_have_value( + self.page.locator("body"), + "bslib-flow-mobile", + has_class=not value, + timeout=timeout, + ) + + def expect_window_title( + self, title: PatternOrStr, *, timeout: Timeout = None + ) -> None: + """ + Expects the window title to have the specified text. + + Parameters + ---------- + title + The expected window title. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + playwright_expect(self.page).to_have_title(title, timeout=timeout) + + def expect_lang(self, lang: PatternOrStr, *, timeout: Timeout = None) -> None: + """ + Expects the HTML tag to have the specified language. + + Parameters + ---------- + lang + The expected language. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + expect_attribute_to_have_value( + self.page.locator("html"), + "lang", + lang, + timeout=timeout, + ) diff --git a/shiny/playwright/expect/_internal.py b/shiny/playwright/expect/_internal.py index 908e3f511..8ae0af239 100644 --- a/shiny/playwright/expect/_internal.py +++ b/shiny/playwright/expect/_internal.py @@ -93,3 +93,41 @@ def expect_class_to_have_value( expect_to_have_class(loc, class_, timeout=timeout) else: expect_not_to_have_class(loc, class_, timeout=timeout) + + +def _expect_nav_to_have_header_footer( + parent_loc: Locator, + header_id: str, + footer_id: str, + *, + timeout: Timeout = None, +) -> None: + """ + Expect the DOM structure for a header and footer to be preserved. + + Parameters + ---------- + parent_loc + The parent locator to check. + header_id + The ID of the header element. + footer_id + The ID of the footer element. + timeout + The maximum time to wait for the header and footer to appear. + """ + # assert the DOM structure for page_navbar with header and footer is preserved + class_attr = parent_loc.get_attribute("class") + if class_attr and "card" in class_attr: + complicated_parent_loc = parent_loc.locator( + "xpath=.", + has=parent_loc.locator("..").locator( + f".card-body:has(#{header_id}) + .card-body:has(.tab-content) + .card-body #{footer_id}" + ), + ) + else: + complicated_parent_loc = parent_loc.locator( + "xpath=.", + has=parent_loc.locator(f"#{header_id} + .tab-content + #{footer_id}"), + ).locator("..") + playwright_expect(complicated_parent_loc).to_have_count(1, timeout=timeout) diff --git a/tests/playwright/shiny/components/nav/app.py b/tests/playwright/shiny/components/nav/app.py index 2344ea9a1..2fe5f59ad 100644 --- a/tests/playwright/shiny/components/nav/app.py +++ b/tests/playwright/shiny/components/nav/app.py @@ -8,11 +8,6 @@ from shiny.types import NavSetArg from shiny.ui import Sidebar -# TODO-karan; Make test that uses sidebar / no sidebar (where possible) -# TODO-karan; Make test that has/does not have a header & footer (where possible) -# TODO-karan; Test for title value (where possible) -# TODO-karan; Make test that has placement = "above" / "below" (where possible); Test in combination of with/without sidebar - def nav_controls(prefix: str) -> List[NavSetArg]: return [ diff --git a/tests/playwright/shiny/components/nav/navset_bar_kitchensink/test_navset_bar.py b/tests/playwright/shiny/components/nav/navset_bar_kitchensink/test_navset_bar.py index b7d323ca9..488b4799b 100644 --- a/tests/playwright/shiny/components/nav/navset_bar_kitchensink/test_navset_bar.py +++ b/tests/playwright/shiny/components/nav/navset_bar_kitchensink/test_navset_bar.py @@ -45,13 +45,13 @@ def test_navset_bar_kitchensink(page: Page, local_app: ShinyAppProc) -> None: "Panel A content" ) navset_bar_with_sidebar_collapsible_bg_inverse.expect_position("sticky-top") - navset_bar_with_sidebar_collapsible_bg_inverse.expect_inverse() + navset_bar_with_sidebar_collapsible_bg_inverse.expect_inverse(value=True) navset_bar_with_sidebar_collapsible_bg_inverse.expect_bg("DodgerBlue") navset_bar_with_sidebar_collapsible_bg_inverse.expect_sidebar(True) - navset_bar_with_sidebar_collapsible_bg_inverse.expect_layout("fluid") + navset_bar_with_sidebar_collapsible_bg_inverse.expect_fluid(True) navset_tab.set("fixed") navset_bar_collapsible_underline_fixed_gap._expect_content_text("Panel A content") navset_bar_collapsible_underline_fixed_gap.expect_value("A") navset_bar_collapsible_underline_fixed_gap.expect_gap("50px") - navset_bar_collapsible_underline_fixed_gap.expect_layout("fixed") + navset_bar_collapsible_underline_fixed_gap.expect_fluid(False) diff --git a/tests/playwright/shiny/components/nav/navset_hidden_kitchensink/app.py b/tests/playwright/shiny/components/nav/navset_hidden_kitchensink/app.py new file mode 100644 index 000000000..339e6bcca --- /dev/null +++ b/tests/playwright/shiny/components/nav/navset_hidden_kitchensink/app.py @@ -0,0 +1,40 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_radio_buttons("controller1", "Controller1", ["1", "2", "3"], selected="2") + +with ui.navset_hidden( + id="hidden_tabs1", + header=ui.tags.div("Navset_hidden_header", id="navset_hidden_header1"), + footer=ui.tags.div("Navset_hidden_footer", id="navset_hidden_footer1"), + selected="panel2", +): + with ui.nav_panel(None, value="panel1"): + "Panel 1 content" + with ui.nav_panel(None, value="panel2"): + "Panel 2 content" + with ui.nav_panel(None, value="panel3"): + "Panel 3 content" + +ui.markdown("-----") +ui.input_radio_buttons("controller2", "Controller2", ["4", "5", "6"]) + +with ui.navset_hidden(id="hidden_tabs2"): + with ui.nav_panel(None, value="panel4"): + "Panel 4 content" + with ui.nav_panel(None, value="panel5"): + "Panel 5 content" + with ui.nav_panel(None, value="panel6"): + "Panel 6 content" + + +@reactive.effect +@reactive.event(input.controller1) +def _(): + ui.update_navs("hidden_tabs1", selected="panel" + str(input.controller1())) + + +@reactive.effect +@reactive.event(input.controller2) +def _(): + ui.update_navs("hidden_tabs2", selected="panel" + str(input.controller2())) diff --git a/tests/playwright/shiny/components/nav/navset_hidden_kitchensink/test_navset_hidden_kitchensink.py b/tests/playwright/shiny/components/nav/navset_hidden_kitchensink/test_navset_hidden_kitchensink.py new file mode 100644 index 000000000..0eadade65 --- /dev/null +++ b/tests/playwright/shiny/components/nav/navset_hidden_kitchensink/test_navset_hidden_kitchensink.py @@ -0,0 +1,33 @@ +from playwright.sync_api import Page, expect + +from shiny.playwright import controller +from shiny.playwright.expect._internal import _expect_nav_to_have_header_footer +from shiny.run import ShinyAppProc + + +def test_navset_hidden_kitchensink(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + navset_hidden_1 = controller.NavsetHidden(page, "hidden_tabs1") + radio1 = controller.InputRadioButtons(page, "controller1") + navset_hidden_1.expect_value("panel2") + navset_hidden_1._expect_content_text("Panel 2 content") + # assert the DOM structure for hidden_navset with header and footer is preserved + _expect_nav_to_have_header_footer( + navset_hidden_1.get_loc_active_content().locator("..").locator(".."), + "navset_hidden_header1", + "navset_hidden_footer1", + ) + # assert header and footer contents + expect(page.locator("#navset_hidden_header1")).to_contain_text( + "Navset_hidden_header" + ) + expect(page.locator("#navset_hidden_footer1")).to_contain_text( + "Navset_hidden_footer" + ) + radio1.set("1") + navset_hidden_1.expect_value("panel1") + navset_hidden_1._expect_content_text("Panel 1 content") + + navset_hidden_2 = controller.NavsetHidden(page, "hidden_tabs2") + navset_hidden_2.expect_value("panel4") diff --git a/tests/playwright/shiny/components/nav/navsets_kitchensink/app.py b/tests/playwright/shiny/components/nav/navsets_kitchensink/app.py index 6dc08f2d6..711c34502 100644 --- a/tests/playwright/shiny/components/nav/navsets_kitchensink/app.py +++ b/tests/playwright/shiny/components/nav/navsets_kitchensink/app.py @@ -1,13 +1,12 @@ from typing import Any, Dict +from shiny import ui as core_ui from shiny.express import expressify, ui -ui.page_opts(title="Navsets kitchensink App") +ui.page_opts(title="Navsets kitchensink App", id="navsets_collection") def navset_sidebar(): - from shiny import ui as core_ui - return core_ui.sidebar(core_ui.markdown("Sidebar content")) @@ -15,32 +14,56 @@ def navset_sidebar(): "navset_pill": { "default": {}, "with_header_footer": { - "header": "navset_pill_with_header_footer header", - "footer": "navset_pill_with_header_footer footer", + "header": { + "content": "navset_pill_with_header_footer header", + "id": "navset_pill_header", + }, + "footer": { + "content": "navset_pill_with_header_footer footer", + "id": "navset_pill_footer", + }, }, "selected": {"selected": "navset_pill_b"}, }, "navset_underline": { "default": {}, "with_header_footer": { - "header": "navset_underline_with_header_footer header", - "footer": "navset_underline_with_header_footer footer", + "header": { + "content": "navset_underline_with_header_footer header", + "id": "navset_underline_header", + }, + "footer": { + "content": "navset_underline_with_header_footer footer", + "id": "navset_underline_footer", + }, }, "selected": {"selected": "navset_underline_b"}, }, "navset_tab": { "default": {}, "with_header_footer": { - "header": "navset_tab_with_header_footer header", - "footer": "navset_tab_with_header_footer footer", + "header": { + "content": "navset_tab_with_header_footer header", + "id": "navset_tab_header", + }, + "footer": { + "content": "navset_tab_with_header_footer footer", + "id": "navset_tab_footer", + }, }, "selected": {"selected": "navset_tab_b"}, }, "navset_pill_list": { "default": {}, "with_header_footer": { - "header": "navset_pill_list_with_header_footer header", - "footer": "navset_pill_list_with_header_footer footer", + "header": { + "content": "navset_pill_list_with_header_footer header", + "id": "navset_pill_list_header", + }, + "footer": { + "content": "navset_pill_list_with_header_footer footer", + "id": "navset_pill_list_footer", + }, }, "selected": {"selected": "navset_pill_list_b"}, "widths_no_well": {"widths": (10, 2), "well": False}, @@ -48,8 +71,14 @@ def navset_sidebar(): "navset_card_pill": { "with_header_footer": { "title": "navset_card_pill_with_header_footer", - "header": "navset_card_pill_with_header_footer header", - "footer": "navset_card_pill_with_header_footer footer", + "header": { + "content": "navset_card_pill_with_header_footer header", + "id": "navset_card_pill_header", + }, + "footer": { + "content": "navset_card_pill_with_header_footer footer", + "id": "navset_card_pill_footer", + }, }, "default": {}, "placement": {"placement": "below"}, @@ -59,8 +88,14 @@ def navset_sidebar(): "navset_card_tab": { "with_header_footer": { "title": "navset_card_tab_with_header_footer", - "header": "navset_card_tab_with_header_footer header", - "footer": "navset_card_tab_with_header_footer footer", + "header": { + "content": "navset_card_tab_with_header_footer header", + "id": "navset_card_tab_header", + }, + "footer": { + "content": "navset_card_tab_with_header_footer footer", + "id": "navset_card_tab_footer", + }, }, "default": {}, "selected": {"selected": "navset_card_tab_b"}, @@ -69,15 +104,14 @@ def navset_sidebar(): "navset_card_underline": { "with_header_footer": { "title": "navset_card_underline_with_header_footer", - "header": "navset_card_underline_with_header_footer header", - # "header": core_ui.CardItem( - # core_ui.TagList( - # "navset_card_underline_with_header_footer header1", - # core_ui.br(), - # "navset_card_underline_with_header_footer header2", - # ) - # ), - "footer": "navset_card_underline_with_header_footer footer", + "header": { + "content": "navset_card_underline_with_header_footer header", + "id": "navset_card_underline_header", + }, + "footer": { + "content": "navset_card_underline_with_header_footer footer", + "id": "navset_card_underline_footer", + }, }, "default": {}, "selected": {"selected": "navset_card_underline_b"}, @@ -92,7 +126,19 @@ def create_navset(navset_type: str) -> None: navset_function = getattr(ui, navset_type) for navset_id, params in navset_configs[navset_type].items(): - with navset_function(id=f"{navset_type}_{navset_id}", **params): + navset_kwargs = params.copy() + + if "header" in navset_kwargs: + header_content = navset_kwargs["header"]["content"] + header_id = navset_kwargs["header"]["id"] + navset_kwargs["header"] = ui.tags.div(header_content, id=f"{header_id}") + + if "footer" in navset_kwargs: + footer_content = navset_kwargs["footer"]["content"] + footer_id = navset_kwargs["footer"]["id"] + navset_kwargs["footer"] = ui.tags.div(footer_content, id=f"{footer_id}") + + with navset_function(id=f"{navset_type}_{navset_id}", **navset_kwargs): for suffix in ["a", "b", "c"]: with ui.nav_panel(f"{navset_type}_{suffix}"): ui.markdown(f"{navset_type}_{suffix} content") diff --git a/tests/playwright/shiny/components/nav/navsets_kitchensink/test_kitchensink.py b/tests/playwright/shiny/components/nav/navsets_kitchensink/test_navsets_kitchensink.py similarity index 50% rename from tests/playwright/shiny/components/nav/navsets_kitchensink/test_kitchensink.py rename to tests/playwright/shiny/components/nav/navsets_kitchensink/test_navsets_kitchensink.py index 36b71833a..d5fe27c57 100644 --- a/tests/playwright/shiny/components/nav/navsets_kitchensink/test_kitchensink.py +++ b/tests/playwright/shiny/components/nav/navsets_kitchensink/test_navsets_kitchensink.py @@ -1,6 +1,7 @@ -from playwright.sync_api import Page +from playwright.sync_api import Page, expect from shiny.playwright import controller +from shiny.playwright.expect._internal import _expect_nav_to_have_header_footer from shiny.run import ShinyAppProc navsets = [ @@ -18,36 +19,60 @@ ] +def format_navset_name(navset_name: str) -> str: + navset_name = navset_name.replace("_", " ") + navset_name = navset_name.title() + navset_name = navset_name.replace(" ", "") + return navset_name + + def test_navset_kitchensink(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) # Update the page size to be wider page.set_viewport_size({"width": 1500, "height": 800}) + page_navbar = controller.PageNavbar(page, "navsets_collection") + + # assert window title is same as page title if window title is not explicity set + page_navbar.expect_window_title("Navsets kitchensink App") + # for cases across all navsets for navset_name, default_content, selected_content in navsets: navset = controller.NavPanel(page, "navsets_collection", navset_name) navset.click() - navset_default = getattr( - controller, f"{navset_name.replace('_', ' ').title().replace(' ', '')}" - )(page, f"{navset_name}_default") + navset_default = getattr(controller, format_navset_name(navset_name))( + page, f"{navset_name}_default" + ) navset_default._expect_content_text(default_content) navset_default.expect_value(f"{navset_name}_a") - navset_selected = getattr( - controller, f"{navset_name.replace('_', ' ').title().replace(' ', '')}" - )(page, f"{navset_name}_selected") + navset_selected = getattr(controller, format_navset_name(navset_name))( + page, f"{navset_name}_selected" + ) navset_selected._expect_content_text(selected_content) navset_selected.expect_value(f"{navset_name}_b") - # TODO-future: uncomment test to check for header & footer content once class is added - # navset_with_header_footer.expect_header(f"{navset_name}_with_header_footer header") - # navset_with_header_footer.expect_footer(f"{navset_name}_with_header_footer footer") + navset_with_header_footer = getattr( + controller, format_navset_name(navset_name) + )(page, f"{navset_name}_with_header_footer") + _expect_nav_to_have_header_footer( + navset_with_header_footer.get_loc_active_content() + .locator("..") + .locator(".."), + f"{navset_name}_header", + f"{navset_name}_footer", + ) + + # assert header and footer contents + expect(page.locator(f"#{navset_name}_header")).to_have_text( + f"{navset_name}_with_header_footer header" + ) + expect(page.locator(f"#{navset_name}_footer")).to_have_text( + f"{navset_name}_with_header_footer footer" + ) if navset_name.startswith("navset_card"): - navset_with_header_footer = getattr( - controller, f"{navset_name.replace('_', ' ').title().replace(' ', '')}" - )(page, f"{navset_name}_with_header_footer") navset_with_header_footer.expect_title(f"{navset_name}_with_header_footer") navset_card_underline_with_sidebar = getattr( @@ -60,3 +85,16 @@ def test_navset_kitchensink(page: Page, local_app: ShinyAppProc) -> None: controller, f"{navset_name.replace('_', ' ').title().replace(' ', '')}" )(page, f"{navset_name}_placement") navset_card_underline_placement.expect_placement("below") + + if navset_name in {"navset_pill_list"}: + navset_pill_list_default = controller.NavsetPillList( + page, f"{navset_name}_default" + ) + navset_pill_list_default.expect_well(True) + navset_pill_list_default.expect_widths([4, 8]) + + navset_pill_list_with_well = controller.NavsetPillList( + page, f"{navset_name}_widths_no_well" + ) + navset_pill_list_with_well.expect_well(False) + navset_pill_list_with_well.expect_widths([10, 2]) diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/default_page_navbar/app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/default_page_navbar/app.py new file mode 100644 index 000000000..bbb9f6d70 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/default_page_navbar/app.py @@ -0,0 +1,16 @@ +from shiny.express import ui + +ui.page_opts( + id="default_page_navbar", + title="Default Page Navbar", + window_title="Page NavBar title", + lang="en", + fillable_mobile=True, + fillable=True, +) + +with ui.nav_panel("Data"): + "This page could be used to pick a dataset." + +with ui.nav_panel("View"): + "This page could be used to view the dataset." diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/default_page_navbar/test_default_page_navbar_app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/default_page_navbar/test_default_page_navbar_app.py new file mode 100644 index 000000000..4e1b242ba --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/default_page_navbar/test_default_page_navbar_app.py @@ -0,0 +1,15 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_default_page_navbar(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + default_page_navbar = controller.PageNavbar(page, "default_page_navbar") + default_page_navbar.expect_title("Default Page Navbar") + default_page_navbar.expect_inverse(value=False) + default_page_navbar.expect_lang("en") + default_page_navbar.expect_window_title("Page NavBar title") + default_page_navbar.expect_fillable_mobile(True) diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fillable/app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fillable/app.py new file mode 100644 index 000000000..a875fc834 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fillable/app.py @@ -0,0 +1,13 @@ +from shiny.express import ui + +ui.page_opts( + id="page_navbar_fillable", + fillable=True, + gap="300px", +) + +with ui.nav_panel("Data"): + "This page could be used to pick a dataset." + +with ui.nav_panel("View"): + "This page could be used to view the dataset." diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fillable/test_page_navbar_fillable_app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fillable/test_page_navbar_fillable_app.py new file mode 100644 index 000000000..b3151a5ba --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fillable/test_page_navbar_fillable_app.py @@ -0,0 +1,15 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_page_navbar_fillable(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + page_navbar_fillable = controller.PageNavbar(page, "page_navbar_fillable") + page_navbar_fillable._expect_content_text( + "This page could be used to pick a dataset." + ) + page_navbar_fillable.expect_fillable(True) + page_navbar_fillable.expect_gap("300px") diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fixed_bottom_inverse_bg/app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fixed_bottom_inverse_bg/app.py new file mode 100644 index 000000000..6fe126b18 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fixed_bottom_inverse_bg/app.py @@ -0,0 +1,14 @@ +from shiny.express import ui + +ui.page_opts( + id="page_fixed_bottom_inverse_bg", + position="fixed-bottom", + bg="dodgerBlue", + inverse=True, +) + +with ui.nav_panel("Data"): + "This page could be used to pick a dataset." + +with ui.nav_panel("View"): + "This page could be used to view the dataset." diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fixed_bottom_inverse_bg/test_page_navbar_fixed_bottom_inverse_bg_app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fixed_bottom_inverse_bg/test_page_navbar_fixed_bottom_inverse_bg_app.py new file mode 100644 index 000000000..6da8b854a --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_fixed_bottom_inverse_bg/test_page_navbar_fixed_bottom_inverse_bg_app.py @@ -0,0 +1,17 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_page_navbar_fixed_bottom_inverse_bg( + page: Page, local_app: ShinyAppProc +) -> None: + page.goto(local_app.url) + + page_navbar_fixed_bottom_inverse_bg = controller.PageNavbar( + page, "page_fixed_bottom_inverse_bg" + ) + page_navbar_fixed_bottom_inverse_bg.expect_position("fixed-bottom") + page_navbar_fixed_bottom_inverse_bg.expect_inverse(value=True) + page_navbar_fixed_bottom_inverse_bg.expect_bg("dodgerBlue") diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_header_footer_fixed_top/app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_header_footer_fixed_top/app.py new file mode 100644 index 000000000..cac3477a0 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_header_footer_fixed_top/app.py @@ -0,0 +1,15 @@ +from shiny.express import ui + +ui.page_opts( + id="page_navbar_header_footer_fixed_top", + header=ui.tags.div("Header", id="page_navbar_header"), + footer=ui.tags.div("Footer", id="page_navbar_footer"), + position="fixed-top", + fluid=False, +) + +with ui.nav_panel("Data"): + "This page could be used to pick a dataset." + +with ui.nav_panel("View"): + "This page could be used to view the dataset." diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_header_footer_fixed_top/test_page_navbar_header_footer_fixed_top_app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_header_footer_fixed_top/test_page_navbar_header_footer_fixed_top_app.py new file mode 100644 index 000000000..4fa0e6f52 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_header_footer_fixed_top/test_page_navbar_header_footer_fixed_top_app.py @@ -0,0 +1,30 @@ +from playwright.sync_api import Page, expect + +from shiny.playwright import controller +from shiny.playwright.expect._internal import _expect_nav_to_have_header_footer +from shiny.run import ShinyAppProc + + +def test_page_navbar_header_footer_fixed_top( + page: Page, local_app: ShinyAppProc +) -> None: + page.goto(local_app.url) + + page_navbar_header_footer_fixed_top = controller.PageNavbar( + page, "page_navbar_header_footer_fixed_top" + ) + page_navbar_header_footer_fixed_top.expect_position("fixed-top") + _expect_nav_to_have_header_footer( + page_navbar_header_footer_fixed_top.get_loc_active_content() + .locator("..") + .locator(".."), + "page_navbar_header", + "page_navbar_footer", + ) + + # assert header and footer contents + expect(page.locator("#page_navbar_header")).to_have_text("Header") + expect(page.locator("#page_navbar_footer")).to_have_text("Footer") + + # not working as expected since not showing on app + page_navbar_header_footer_fixed_top.expect_fluid(False) diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_selected/app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_selected/app.py new file mode 100644 index 000000000..4eb164158 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_selected/app.py @@ -0,0 +1,9 @@ +from shiny.express import ui + +ui.page_opts(id="page_navbar_selected", selected="View", fluid=False) + +with ui.nav_panel("Data"): + "This page could be used to pick a dataset." + +with ui.nav_panel("View"): + "This page could be used to view the dataset." diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_selected/test_page_navbar_selected_app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_selected/test_page_navbar_selected_app.py new file mode 100644 index 000000000..bb3e7618f --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_selected/test_page_navbar_selected_app.py @@ -0,0 +1,14 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_page_navbar_selected(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + page_navbar_selected = controller.PageNavbar(page, "page_navbar_selected") + page_navbar_selected.expect_value("View") + page_navbar_selected._expect_content_text( + "This page could be used to view the dataset." + ) diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_sidebar/app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_sidebar/app.py new file mode 100644 index 000000000..527df9539 --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_sidebar/app.py @@ -0,0 +1,16 @@ +from shiny.express import ui + + +def navset_sidebar(): + from shiny import ui as core_ui + + return core_ui.sidebar(core_ui.markdown("Sidebar content")) + + +ui.page_opts(id="page_navbar_sidebar", sidebar=navset_sidebar()) + +with ui.nav_panel("Data"): + "This page could be used to pick a dataset." + +with ui.nav_panel("View"): + "This page could be used to view the dataset." diff --git a/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_sidebar/test_page_navbar_sidebar_app.py b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_sidebar/test_page_navbar_sidebar_app.py new file mode 100644 index 000000000..51f51e39b --- /dev/null +++ b/tests/playwright/shiny/components/nav/page_navbar_kitchensink/page_navbar_sidebar/test_page_navbar_sidebar_app.py @@ -0,0 +1,15 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_page_navbar_sidebar(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + page_navbar_sidebar = controller.PageNavbar(page, "page_navbar_sidebar") + page_navbar_sidebar.expect_value("Data") + page_navbar_sidebar._expect_content_text( + "This page could be used to pick a dataset." + ) + page_navbar_sidebar.expect_sidebar(True)