diff --git a/tests/playwright/controls.py b/tests/playwright/controls.py index 72ebdb3b6..986fc460f 100644 --- a/tests/playwright/controls.py +++ b/tests/playwright/controls.py @@ -351,22 +351,54 @@ def expect_label( class _WidthLocM: + """ + A mixin class representing the `loc`'s width. + This class provides methods to expect the width attribute of an element. + """ + def expect_width( self: _InputBaseP, value: AttrValue, *, timeout: Timeout = None, ) -> None: + """ + Expect the width attribute of an element to have a specific value. + + Parameters + ---------- + value + The expected value of the width attribute. + timeout + The maximum time to wait for the expectation to be fulfilled. + Defaults to None. + """ expect_attr(self.loc, "width", value=value, timeout=timeout) class _WidthContainerM: + """ + A mixin class representing the container's width. + This class provides methods to expect the width attribute of an element's container. + """ + def expect_width( self: _InputWithContainerP, value: AttrValue, *, timeout: Timeout = None, ) -> None: + """ + Expect the width attribute of an element's container to have a specific value. + + Parameters + ---------- + value + The expected value of the width attribute. + timeout + The maximum time to wait for the expectation to be fulfilled. + Defaults to None. + """ expect_attr(self.loc_container, "width", value=value, timeout=timeout) @@ -2676,7 +2708,7 @@ def close_full_screen( """ self._loc_close_button.click(timeout=timeout) - def expect_full_screen( + def expect_full_screen_open( self: _CardFullScreenLayoutP, open: bool, *, timeout: Timeout = None ) -> None: """ @@ -2693,6 +2725,23 @@ def expect_full_screen( int(open), timeout=timeout ) + def expect_full_screen_available( + self: _CardFullScreenLayoutP, available: bool, *, timeout: Timeout = None + ) -> None: + """ + Expects the card to be available for full screen mode. + + Parameters + ---------- + available + True if the value box is expected to be available for full screen mode, False otherwise. + timeout + The maximum time to wait for the expectation to pass. Defaults to None. + """ + playwright_expect(self._loc_fullscreen).to_have_count( + int(available), timeout=timeout + ) + class ValueBox( _WidthLocM, @@ -2700,24 +2749,24 @@ class ValueBox( _InputWithContainer, ): """ - ValueBox control for shiny.ui.value_box - https://shiny.posit.co/py/api/core/ui.value_box.html + ValueBox control for :func:`~shiny.ui.value_box` """ loc: Locator """ - Locator for the value box's value + `Locator` for the value box's value """ loc_showcase: Locator """ - Locator for the value box showcase + `Locator` for the value box showcase """ loc_title: Locator """ - Locator for the value box title + `Locator` for the value box title """ loc_body: Locator """ - Locator for the value box body + `Locator` for the value box body """ def __init__(self, page: Page, id: str) -> None: @@ -2835,35 +2884,40 @@ def expect_body( timeout=timeout, ) - def expect_full_screen_available( - self, available: bool, *, timeout: Timeout = None - ) -> None: + +class Card(_WidthLocM, _CardFooterM, _CardBodyM, _CardFullScreenM, _InputWithContainer): + """ + Card control for :func:`~shiny.ui.card` + """ + + loc: Locator + """ + `Locator` for the card's value + """ + loc_title: Locator + """ + `Locator` for the card title + """ + loc_footer: Locator + """ + `Locator` for the card footer + """ + loc_body: Locator + """ + `Locator` for the card body + """ + + def __init__(self, page: Page, id: str) -> None: """ - Expects the value box to be available for full screen mode. + Initializes a new instance of the Card class. Parameters ---------- - available - True if the value box is expected to be available for full screen mode, False otherwise. - timeout - The maximum time to wait for the expectation to pass. Defaults to None. + page + The Playwright page object. + id + The ID of the card. """ - playwright_expect(self._loc_fullscreen).to_have_count( - int(available), timeout=timeout - ) - - -class Card(_WidthLocM, _CardFooterM, _CardBodyM, _CardFullScreenM, _InputWithContainer): - # *args: TagChild | TagAttrs | CardItem, - # full_screen: bool = False, - # height: CssUnit | None = None, - # max_height: CssUnit | None = None, - # min_height: CssUnit | None = None, - # fill: bool = True, - # class_: str | None = None, - # wrapper: WrapperCallable | MISSING_TYPE | None = MISSING, - # **kwargs: TagAttrValue - def __init__(self, page: Page, id: str) -> None: super().__init__( page, id=id, @@ -2886,14 +2940,25 @@ def __init__(self, page: Page, id: str) -> None: def expect_header( self, - text: PatternOrStr, + text: PatternOrStr | None, *, timeout: Timeout = None, ) -> None: - playwright_expect(self.loc_title).to_have_text( - text, - timeout=timeout, - ) + """ + Expects the card header to have a specific text. + + Parameters + ---------- + text + The expected text pattern or string + Note: None if the header is expected to not exist. + timeout + The maximum time to wait for the expectation to pass. Defaults to None. + """ + if text is None: + playwright_expect(self.loc_title).to_have_count(0, timeout=timeout) + else: + playwright_expect(self.loc_title).to_have_text(text, timeout=timeout) # def expect_body( # self, @@ -2908,17 +2973,66 @@ def expect_header( # timeout=timeout, # ) + def expect_footer( + self, + text: PatternOrStr | None, + *, + timeout: Timeout = None, + ) -> None: + """ + Expects the card footer to have a specific text. + + Parameters + ---------- + text + The expected text pattern or string + Note: None if the footer is expected to not exist. + timeout + The maximum time to wait for the expectation to pass. Defaults to None. + """ + if text is None: + playwright_expect(self.loc_footer).to_have_count(0, timeout=timeout) + else: + playwright_expect(self.loc_footer).to_have_text(text, timeout=timeout) + def expect_max_height(self, value: StyleValue, *, timeout: Timeout = None) -> None: + """ + Expects the card to have a specific maximum height. + + Parameters + ---------- + value + The expected maximum height value. + timeout + The maximum time to wait for the expectation to pass. Defaults to None. + """ expect_to_have_style(self.loc_container, "max-height", value, timeout=timeout) def expect_min_height(self, value: StyleValue, *, timeout: Timeout = None) -> None: + """ + Expects the card to have a specific minimum height. + + Parameters + ---------- + value + The expected minimum height value. + timeout + The maximum time to wait for the expectation to pass. Defaults to None. + """ expect_to_have_style(self.loc_container, "min-height", value, timeout=timeout) def expect_height(self, value: StyleValue, *, timeout: Timeout = None) -> None: - expect_to_have_style(self.loc_container, "height", value, timeout=timeout) - + """ + Expects the card to have a specific height. -# Experimental below + Parameters + ---------- + value + The expected height value. + timeout + The maximum time to wait for the expectation to pass. Defaults to None. + """ + expect_to_have_style(self.loc_container, "height", value, timeout=timeout) class Accordion( diff --git a/tests/playwright/deploys/shiny-client-console-error/app.py b/tests/playwright/deploys/shiny-client-console-error/app.py new file mode 100644 index 000000000..8d9c5e9d4 --- /dev/null +++ b/tests/playwright/deploys/shiny-client-console-error/app.py @@ -0,0 +1,11 @@ +from shiny import reactive, render +from shiny.express import input, ui + +ui.input_action_button("same_id", "Action") +ui.input_action_button("same_id", "Action") + + +@render.text() +@reactive.event(input.same_id) +def counter(): + return f"{input.same_id()}" diff --git a/tests/playwright/deploys/shiny-client-console-error/app_requirements.txt b/tests/playwright/deploys/shiny-client-console-error/app_requirements.txt new file mode 100644 index 000000000..03309f726 --- /dev/null +++ b/tests/playwright/deploys/shiny-client-console-error/app_requirements.txt @@ -0,0 +1,2 @@ +git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools + diff --git a/tests/playwright/deploys/shiny-client-console-error/rsconnect-python/shiny-client-console-error.json b/tests/playwright/deploys/shiny-client-console-error/rsconnect-python/shiny-client-console-error.json new file mode 100644 index 000000000..27dc7bf94 --- /dev/null +++ b/tests/playwright/deploys/shiny-client-console-error/rsconnect-python/shiny-client-console-error.json @@ -0,0 +1,11 @@ +{ + "https://api.shinyapps.io": { + "server_url": "https://api.shinyapps.io", + "app_url": "https://testing-apps.shinyapps.io/shiny_client_console_error/", + "app_id": 11634266, + "app_guid": null, + "title": "shiny_client_console_error", + "app_mode": "python-shiny", + "app_store_version": 1 + } +} diff --git a/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py b/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py new file mode 100644 index 000000000..1feb6f3aa --- /dev/null +++ b/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py @@ -0,0 +1,28 @@ +from playwright.sync_api import Page, expect +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +app_url = create_deploys_app_url_fixture("shiny_client_console_error") + + +@skip_if_not_chrome +def test_shiny_client_console_error(page: Page, app_url: str) -> None: + page.goto(app_url) + + assert page.locator("#same_id").count() == 2 + shiny_error_message = page.locator("shiny-error-message") + + # show the client error message only for local apps + if "127.0.0.1" in app_url: + + expect(shiny_error_message).not_to_have_count(0) + expect(shiny_error_message).to_have_attribute( + "message", 'The following ID was repeated:\n- "same_id": 2 inputs' + ) + expect(page.get_by_role("button", name="Dismiss all")).to_have_count(1) + expect( + page.get_by_role("button", name="Copy error to clipboard") + ).to_have_count(1) + + # for deployed apps to shinyapps.io or connect hide the client error message + else: + expect(shiny_error_message).to_have_count(0) diff --git a/tests/playwright/shiny/components/card-input/test_card-input.py b/tests/playwright/shiny/components/card-input/test_card-input.py index 7237ff9f1..1cc794aff 100644 --- a/tests/playwright/shiny/components/card-input/test_card-input.py +++ b/tests/playwright/shiny/components/card-input/test_card-input.py @@ -24,25 +24,25 @@ def test_card_input(page: Page, app_path: str, sel_card: str, sel_vb: str) -> No out_vb = OutputCode(page, "out_value_box") # Open and close card full screen, check input value ------ - card.expect_full_screen(False) + card.expect_full_screen_open(False) out_card.expect_value("False") card.open_full_screen() - card.expect_full_screen(True) + card.expect_full_screen_open(True) out_card.expect_value("True") card.close_full_screen() - card.expect_full_screen(False) + card.expect_full_screen_open(False) out_card.expect_value("False") # Open and close value box full screen, check input value ------ - vb.expect_full_screen(False) + vb.expect_full_screen_open(False) out_vb.expect_value("False") vb.open_full_screen() - vb.expect_full_screen(True) + vb.expect_full_screen_open(True) out_vb.expect_value("True") vb.close_full_screen() - vb.expect_full_screen(False) + vb.expect_full_screen_open(False) out_vb.expect_value("False") diff --git a/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py b/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py index 9f0b759da..776571cdf 100644 --- a/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py +++ b/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py @@ -58,12 +58,12 @@ def test_valuebox(page: Page, local_app: ShinyAppProc) -> None: assert get_value_box_bg_color(value_box1) == "rgb(193, 0, 0)" assert get_value_box_fg_color(value_box1) == "rgb(255, 255, 255)" value_box1.expect_full_screen_available(True) - value_box1.expect_full_screen(False) + value_box1.expect_full_screen_open(False) value_box1.open_full_screen() - value_box1.expect_full_screen(True) + value_box1.expect_full_screen_open(True) value_box1.expect_body(["Inside the fullscreen"]) value_box1.close_full_screen() - value_box1.expect_full_screen(False) + value_box1.expect_full_screen_open(False) value_box2 = ValueBox(page, "valuebox2") value_box2.expect_height(None) diff --git a/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py b/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py index d9bf7129b..0458f1c87 100644 --- a/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py +++ b/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py @@ -9,9 +9,9 @@ def test_valuebox(page: Page, local_app: ShinyAppProc, value_box_id: str) -> Non page.goto(local_app.url) value_box = ValueBox(page, value_box_id) - value_box.expect_full_screen(False) + value_box.expect_full_screen_open(False) value_box.open_full_screen() - value_box.expect_full_screen(True) + value_box.expect_full_screen_open(True) if value_box_id == "valuebox1": value_box.expect_height(None) value_box.expect_title("KPI Title") @@ -29,4 +29,4 @@ def test_valuebox(page: Page, local_app: ShinyAppProc, value_box_id: str) -> Non ) assert title_tag_name == "p" value_box.close_full_screen() - value_box.expect_full_screen(False) + value_box.expect_full_screen_open(False) diff --git a/tests/playwright/shiny/experimental/card/kitchensink/app.py b/tests/playwright/shiny/experimental/card/kitchensink/app.py new file mode 100644 index 000000000..6e76d68f2 --- /dev/null +++ b/tests/playwright/shiny/experimental/card/kitchensink/app.py @@ -0,0 +1,27 @@ +from shiny import App, ui + +app_ui = ui.page_fluid( + ui.card( + ui.card_header("Check for header"), + "This is the body of a card with default height w/ fullscreen", + ui.card_footer("Check for footer"), + full_screen=True, + id="card1", + ), + ui.card( + ui.p("This is the body without a header of a footer - No Fullscreen"), + full_screen=False, + id="card2", + ), + ui.card( + ui.card_header("Fill is False. Fullscreen is False"), + ui.h3("Max height and min height are set."), + fill=False, + max_height="500px", + min_height="200px", + id="card3", + ), +) + + +app = App(app_ui, server=None) diff --git a/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py b/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py new file mode 100644 index 000000000..a11130dca --- /dev/null +++ b/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py @@ -0,0 +1,62 @@ +from conftest import ShinyAppProc +from controls import Card +from playwright.sync_api import Page + + +def get_body_tag_name(card: Card) -> str: + body_tag_name = ( + card.loc_body.locator("*").nth(0).evaluate("el => el.tagName.toLowerCase()") + ) + return body_tag_name + + +""" +For each card we want to test +Max Height and Min Height: The tests assert the max-height and min-height CSS properties applied to the card. +Header and Footer: The tests verify the presence and content of the header and footer elements within each card. +Body Element: The tests assert the tag name used for the body element within each card (e.g., ,

,

,

,

). +Fullscreen Availability and State: The tests check whether the fullscreen feature is available for a particular card and verify its initial state (fullscreen or not). For cards with fullscreen support, the tests open and close the fullscreen mode and assert the expected behavior. +""" + + +def test_card_kitchensink(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + card = Card(page, "card1") + card.expect_max_height(None) + card.expect_min_height(None) + card.expect_height(None) + card.expect_header("Check for header") + card.expect_footer("Check for footer") + card.expect_body( + [ + "\nThis is the body of a card with default height w/ fullscreen", + ] + ) + card.expect_full_screen_open(False) + card.open_full_screen() + card.expect_full_screen_open(True) + card.close_full_screen() + card.expect_full_screen_open(False) + + card = Card(page, "card2") + card.expect_max_height(None) + card.expect_min_height(None) + card.expect_height(None) + card.expect_header(None) + card.expect_footer(None) + card.expect_body( + ["\nThis is the body without a header of a footer - No Fullscreen\n"] + ) + assert get_body_tag_name(card) == "p" + card.expect_full_screen_open(False) + card.expect_full_screen_available(False) + + card = Card(page, "card3") + card.expect_max_height("500px") + card.expect_min_height("200px") + card.expect_header("Fill is False. Fullscreen is False") + card.expect_footer(None) + card.expect_body(["Max height and min height are set."]) + assert get_body_tag_name(card) == "h3" + card.expect_full_screen_available(False) diff --git a/tests/playwright/shiny/experimental/card/test_card.py b/tests/playwright/shiny/experimental/card/test_card.py index a86bed4fa..6c49f4e0b 100644 --- a/tests/playwright/shiny/experimental/card/test_card.py +++ b/tests/playwright/shiny/experimental/card/test_card.py @@ -19,8 +19,8 @@ def test_card(page: Page, local_app: ShinyAppProc) -> None: "\nThis is still the body.\n", ] ) - card.expect_full_screen(False) + card.expect_full_screen_open(False) card.open_full_screen() - card.expect_full_screen(True) + card.expect_full_screen_open(True) card.close_full_screen() - card.expect_full_screen(False) + card.expect_full_screen_open(False)