diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f165e31..a6132e216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Small improvements to the default pulse busy indicator to better blend with any background. It's also now slightly smaller by default.(#1707) +* Added `.expect_class()`, `.expect_gap()`, `.expect_bg_color()`, `.expect_desktop_state()`, `.expect_mobile_state()`, `.expect_mobile_max_height()`, `.expect_title()`, and `.expect_padding()` for `Sidebar` in `shiny.playwright.controllers` (#1715) + +* Modified `.expect_text()` for `Sidebar` in `shiny.playwright.controllers` to use `.loc_content` instead of `loc` for text. Also modified `.expect_width()` to check the `.loc_container`'s style instead of the `.loc` element. (#1715) + +* Modified `.expect_text()` and `.expect_width()` for `Sidebar` in `shiny.playwright.controllers` to use `loc_content` instead of `loc` for text. (#1715) + * Added `.expect_class()` and `.expect_multiple()` for `Accordion` in `shiny.playwright.controllers` (#1710) * Added [narwhals](https://posit-dev.github.io/py-narwhals) support for `@render.table`. This allows for any eager data frame supported by narwhals to be returned from a `@render.table` output method. (#1570) diff --git a/shiny/playwright/controller/_layout.py b/shiny/playwright/controller/_layout.py index ae82392ad..49c5071e1 100644 --- a/shiny/playwright/controller/_layout.py +++ b/shiny/playwright/controller/_layout.py @@ -6,12 +6,15 @@ from playwright.sync_api import expect as playwright_expect from .._types import PatternOrStr, Timeout +from ..expect._internal import ( + expect_attribute_to_have_value as _expect_attribute_to_have_value, +) from ..expect._internal import expect_class_to_have_value as _expect_class_to_have_value -from ._base import UiWithContainer, WidthLocM +from ..expect._internal import expect_style_to_have_value as _expect_style_to_have_value +from ._base import UiWithContainer class Sidebar( - WidthLocM, UiWithContainer, ): """Controller for :func:`shiny.ui.sidebar`.""" @@ -32,6 +35,14 @@ class Sidebar( """ Playwright `Locator` for the position of the sidebar. """ + loc_content: Locator + """ + Playwright `Locator` for the content of the sidebar. + """ + loc_title: Locator + """ + Playwright `Locator` for the title of the sidebar. + """ def __init__(self, page: Page, id: str) -> None: """ @@ -52,6 +63,8 @@ def __init__(self, page: Page, id: str) -> None: ) self.loc_handle = self.loc_container.locator("button.collapse-toggle") self.loc_position = self.loc.locator("..") + self.loc_content = self.loc.locator("> div.sidebar-content") + self.loc_title = self.loc_content.locator("> header.sidebar-title") def expect_text(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: """ @@ -64,7 +77,167 @@ def expect_text(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: timeout The maximum time to wait for the text to appear. Defaults to `None`. """ - playwright_expect(self.loc).to_have_text(value, timeout=timeout) + playwright_expect(self.loc_content).to_have_text(value, timeout=timeout) + + def expect_class( + self, + class_name: str, + *, + has_class: bool = True, + timeout: Timeout = None, + ) -> None: + """ + Asserts that the sidebar has or does not have a CSS class. + + Parameters + ---------- + class_name + The CSS class to check for. + has_class + `True` if the sidebar should have the CSS class, `False` otherwise. + timeout + The maximum time to wait for the sidebar to appear. Defaults to `None`. + """ + _expect_class_to_have_value( + self.loc, + class_name, + has_class=has_class, + timeout=timeout, + ) + + def expect_width(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + """ + Asserts that the sidebar has the expected width. + + Parameters + ---------- + value + The expected width of the sidebar. + timeout + The maximum time to wait for the width to appear. Defaults to `None`. + """ + _expect_style_to_have_value( + self.loc_container, "--_sidebar-width", value, timeout=timeout + ) + + def expect_gap(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + """ + Asserts that the sidebar has the expected gap. + + Parameters + ---------- + value + The expected gap of the sidebar. + timeout + The maximum time to wait for the gap to appear. Defaults to `None`. + """ + _expect_style_to_have_value(self.loc_content, "gap", value, timeout=timeout) + + def expect_bg_color(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + """ + Asserts that the sidebar has the expected background color. + + Parameters + ---------- + value + The expected background color of the sidebar. + timeout + The maximum time to wait for the background color to appear. Defaults to `None`. + """ + _expect_style_to_have_value( + self.loc_container, "--_sidebar-bg", value, timeout=timeout + ) + + def expect_desktop_state( + self, value: Literal["open", "closed", "always"], *, timeout: Timeout = None + ) -> None: + """ + Asserts that the sidebar has the expected state on desktop. + + Parameters + ---------- + value + The expected state of the sidebar on desktop. + timeout + The maximum time to wait for the state to appear. Defaults to `None`. + """ + _expect_attribute_to_have_value( + self.loc_container, + name="data-open-desktop", + value=value, + timeout=timeout, + ) + + def expect_mobile_state( + self, value: Literal["open", "closed", "always"], *, timeout: Timeout = None + ) -> None: + """ + Asserts that the sidebar has the expected state on mobile. + + Parameters + ---------- + value + The expected state of the sidebar on mobile. + timeout + The maximum time to wait for the state to appear. Defaults to `None`. + """ + _expect_attribute_to_have_value( + self.loc_container, + name="data-open-mobile", + value=value, + timeout=timeout, + ) + + def expect_mobile_max_height( + self, value: PatternOrStr, *, timeout: Timeout = None + ) -> None: + """ + Asserts that the sidebar has the expected maximum height on mobile. + + Parameters + ---------- + value + The expected maximum height of the sidebar on mobile. + timeout + The maximum time to wait for the maximum height to appear. Defaults to `None`. + """ + self.expect_mobile_state("always", timeout=timeout) + _expect_style_to_have_value( + self.loc_container, "--_mobile-max-height", value, timeout=timeout + ) + + def expect_title(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + """ + Asserts that the sidebar has the expected title. + + Parameters + ---------- + value + The expected title of the sidebar. + timeout + The maximum time to wait for the title to appear. Defaults to `None`. + """ + playwright_expect(self.loc_title).to_have_text(value, timeout=timeout) + + def expect_padding( + self, value: str | list[str], *, timeout: Timeout = None + ) -> None: + """ + Asserts that the sidebar has the expected padding. + + Parameters + ---------- + value + The expected padding of the sidebar. + timeout + The maximum time to wait for the padding to appear. Defaults to `None`. + """ + if not isinstance(value, list): + value = [value] + padding_val = " ".join(value) + _expect_style_to_have_value( + self.loc_content, "padding", padding_val, timeout=timeout + ) def expect_position( self, diff --git a/tests/playwright/shiny/inputs/sidebar_kitchensink/app.py b/tests/playwright/shiny/inputs/sidebar_kitchensink/app.py new file mode 100644 index 000000000..a5e1db5ff --- /dev/null +++ b/tests/playwright/shiny/inputs/sidebar_kitchensink/app.py @@ -0,0 +1,68 @@ +from shiny.express import input, render, ui + +ui.page_opts(fillable=True) + +with ui.card(): + with ui.layout_sidebar(): + with ui.sidebar( + id="sidebar_left", + open="desktop", + title="Left sidebar", + bg="dodgerBlue", + class_="text-white", + gap="20px", + padding="10px", + width="200px", + ): + "Left sidebar content" + + @render.code + def state_left(): + return f"input.sidebar_left(): {input.sidebar_left()}" + + +with ui.card(): + with ui.layout_sidebar(): + with ui.sidebar( + id="sidebar_right", + position="right", + open={"desktop": "closed", "mobile": "open"}, + padding=["10px", "20px"], + bg="SlateBlue", + ): + "Right sidebar content" + + @render.code + def state_right(): + return f"input.sidebar_right(): {input.sidebar_right()}" + + +with ui.card(): + with ui.layout_sidebar(): + with ui.sidebar( + id="sidebar_closed", + open="closed", + bg="LightCoral", + padding=["10px", "20px", "30px"], + ): + "Closed sidebar content" + + @render.code + def state_closed(): + return f"input.sidebar_closed(): {input.sidebar_closed()}" + + +with ui.card(): + with ui.layout_sidebar(): + with ui.sidebar( + id="sidebar_always", + open="always", + bg="PeachPuff", + padding=["10px", "20px", "30px", "40px"], + max_height_mobile="175px", + ): + "Always sidebar content" + + @render.code + def state_always(): + return f"input.sidebar_always(): {input.sidebar_always()}" diff --git a/tests/playwright/shiny/inputs/sidebar_kitchensink/test_sidebar_kitchensink.py b/tests/playwright/shiny/inputs/sidebar_kitchensink/test_sidebar_kitchensink.py new file mode 100644 index 000000000..cca90e7c4 --- /dev/null +++ b/tests/playwright/shiny/inputs/sidebar_kitchensink/test_sidebar_kitchensink.py @@ -0,0 +1,50 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_sidebar_kitchensink(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + left_sidebar = controller.Sidebar(page, "sidebar_left") + output_txt_left = controller.OutputTextVerbatim(page, "state_left") + left_sidebar.set(True) + left_sidebar.expect_padding("10px") + left_sidebar.expect_padding(["10px"]) + left_sidebar.expect_title("Left sidebar") + left_sidebar.expect_gap("20px") + left_sidebar.expect_class("text-white", has_class=True) + left_sidebar.expect_bg_color("dodgerBlue") + left_sidebar.expect_desktop_state("open") + left_sidebar.expect_mobile_state("closed") + left_sidebar.expect_width("200px") + output_txt_left.expect_value("input.sidebar_left(): True") + left_sidebar.expect_open(True) + left_sidebar.set(False) + output_txt_left.expect_value("input.sidebar_left(): False") + left_sidebar.expect_handle(True) + left_sidebar.expect_open(False) + left_sidebar.loc_handle.click() + left_sidebar.expect_open(True) + output_txt_left.expect_value("input.sidebar_left(): True") + + right_sidebar = controller.Sidebar(page, "sidebar_right") + right_sidebar.expect_padding(["10px", "20px"]) + right_sidebar.expect_bg_color("SlateBlue") + right_sidebar.expect_mobile_state("open") + right_sidebar.expect_desktop_state("closed") + + closed_sidebar = controller.Sidebar(page, "sidebar_closed") + closed_sidebar.expect_padding(["10px", "20px", "30px"]) + closed_sidebar.expect_bg_color("LightCoral") + closed_sidebar.expect_mobile_state("closed") + closed_sidebar.expect_desktop_state("closed") + + always_sidebar = controller.Sidebar(page, "sidebar_always") + always_sidebar.expect_padding(["10px", "20px", "30px", "40px"]) + always_sidebar.expect_bg_color("PeachPuff") + always_sidebar.expect_open(True) + always_sidebar.expect_desktop_state("always") + always_sidebar.expect_mobile_state("always") + always_sidebar.expect_mobile_max_height("175px")