diff --git a/e2e/controls.py b/e2e/controls.py index 610fe3a90..9fa3d6762 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -938,7 +938,7 @@ def expect_locator_values_in_list( *, page: Page, loc_container: Locator, - el_type: str, + el_type: Locator | str, arr_name: str, arr: ListPatternOrStr, is_checked: bool | MISSING_TYPE = MISSING, @@ -949,13 +949,20 @@ def expect_locator_values_in_list( # Make sure the locator has len(uniq_arr) input elements _MultipleDomItems.assert_arr_is_unique(arr, f"`{arr_name}` must be unique") - is_checked_str = _MultipleDomItems.checked_css_str(is_checked) - item_selector = f"{el_type}{is_checked_str}" + if isinstance(el_type, Locator): + if not isinstance(is_checked, MISSING_TYPE): + raise RuntimeError( + "`is_checked` cannot be specified if `el_type` is a Locator" + ) + loc_item = el_type + else: + is_checked_str = _MultipleDomItems.checked_css_str(is_checked) + loc_item = page.locator(f"{el_type}{is_checked_str}") # If there are no items, then we should not have any elements if len(arr) == 0: - playwright_expect(loc_container.locator(item_selector)).to_have_count( + playwright_expect(loc_container.locator(el_type)).to_have_count( 0, timeout=timeout ) return @@ -964,7 +971,7 @@ def expect_locator_values_in_list( # Find all items in set for item, i in zip(arr, range(len(arr))): # Get all elements of type - has_locator = page.locator(item_selector) + has_locator = loc_item # Get the `n`th matching element has_locator = has_locator.nth(i) # Make sure that element has the correct attribute value @@ -982,7 +989,7 @@ def expect_locator_values_in_list( # Make sure other items are not in set # If we know all elements are contained in the container, # and all elements all unique, then it should have a count of `len(arr)` - loc_inputs = loc_container.locator(item_selector) + loc_inputs = loc_container.locator(loc_item) try: playwright_expect(loc_inputs).to_have_count(len(arr), timeout=timeout) except AssertionError as e: @@ -992,14 +999,14 @@ def expect_locator_values_in_list( playwright_expect(loc_container_orig).to_have_count(1, timeout=timeout) # Expecting the container to contain {len(arr)} items - playwright_expect(loc_container_orig.locator(item_selector)).to_have_count( + playwright_expect(loc_container_orig.locator(loc_item)).to_have_count( len(arr), timeout=timeout ) for item, i in zip(arr, range(len(arr))): # Expecting item `{i}` to be `{item}` playwright_expect( - loc_container_orig.locator(item_selector).nth(i) + loc_container_orig.locator(loc_item).nth(i) ).to_have_attribute(key, item, timeout=timeout) # Could not find the reason why. Raising the original error. @@ -2096,6 +2103,7 @@ def expect_value( *, timeout: Timeout = None, ) -> None: + """Note this function will trim value and output text value before comparing them""" self.expect.to_have_text(value, timeout=timeout) @@ -2565,3 +2573,125 @@ def expect_min_height(self, value: StyleValue, *, timeout: Timeout = None) -> No def expect_height(self, value: StyleValue, *, timeout: Timeout = None) -> None: expect_to_have_style(self.loc_container, "height", value, timeout=timeout) + + +### Experimental below + + +class Accordion( + _WidthLocM, + _InputWithContainer, +): + # *args: AccordionPanel | TagAttrs, + # id: Optional[str] = None, + # open: Optional[bool | str | list[str]] = None, + # multiple: bool = True, + # class_: Optional[str] = None, + # width: Optional[CssUnit] = None, + # height: Optional[CssUnit] = None, + # **kwargs: TagAttrValue, + def __init__(self, page: Page, id: str) -> None: + super().__init__( + page, + id=id, + loc="> div.accordion-item", + loc_container=f"div#{id}.accordion.shiny-bound-input", + ) + self.loc_open = self.loc.locator( + # Return self + "xpath=.", + # Simple approach as position is not needed + has=page.locator( + "> div.accordion-collapse.show", + ), + ) + + def expect_height(self, value: StyleValue, *, timeout: Timeout = None) -> None: + expect_to_have_style(self.loc_container, "height", value, timeout=timeout) + + def expect_width(self, value: StyleValue, *, timeout: Timeout = None) -> None: + expect_to_have_style(self.loc_container, "width", value, timeout=timeout) + + def expect_open( + self, + value: list[PatternOrStr], + *, + timeout: Timeout = None, + ) -> None: + _MultipleDomItems.expect_locator_values_in_list( + page=self.page, + loc_container=self.loc_container, + el_type=self.page.locator( + "> div.accordion-item", + has=self.page.locator("> div.accordion-collapse.show"), + ), + # el_type="> div.accordion-item:has(> div.accordion-collapse.show)", + arr_name="value", + arr=value, + key="data-value", + timeout=timeout, + ) + + def expect_panels( + self, + value: list[PatternOrStr], + *, + timeout: Timeout = None, + ) -> None: + _MultipleDomItems.expect_locator_values_in_list( + page=self.page, + loc_container=self.loc_container, + el_type="> div.accordion-item", + arr_name="value", + arr=value, + key="data-value", + timeout=timeout, + ) + + def accordion_panel( + self, + data_value: str, + ) -> AccordionPanel: + return AccordionPanel(self.page, self.id, data_value) + + +class AccordionPanel( + _WidthLocM, + _InputWithContainer, +): + # self, + # *args: TagChild | TagAttrs, + # data_value: str, + # icon: TagChild | None, + # title: TagChild | None, + # id: str | None, + # **kwargs: TagAttrValue, + def __init__(self, page: Page, id: str, data_value: str) -> None: + super().__init__( + page, + id=id, + loc=f"> div.accordion-item[data-value='{data_value}']", + loc_container=f"div#{id}.accordion.shiny-bound-input", + ) + + self.loc_label = self.loc.locator( + "> .accordion-header > .accordion-button > .accordion-title" + ) + + self.loc_icon = self.loc.locator( + "> .accordion-header > .accordion-button > .accordion-icon" + ) + + self.loc_body = self.loc.locator("> .accordion-collapse") + + def expect_label(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + playwright_expect(self.loc_label).to_have_text(value, timeout=timeout) + + def expect_body(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + playwright_expect(self.loc_body).to_have_text(value, timeout=timeout) + + def expect_icon(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: + playwright_expect(self.loc_icon).to_have_text(value, timeout=timeout) + + def expect_open(self, is_open: bool, *, timeout: Timeout = None) -> None: + _expect_class_value(self.loc_body, "show", is_open, timeout=timeout) diff --git a/shiny/experimental/e2e/accordion/app.py b/e2e/experimental/accordion/app.py similarity index 100% rename from shiny/experimental/e2e/accordion/app.py rename to e2e/experimental/accordion/app.py diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py new file mode 100644 index 000000000..330b68544 --- /dev/null +++ b/e2e/experimental/accordion/test_accordion.py @@ -0,0 +1,78 @@ +from conftest import ShinyAppProc +from controls import Accordion, InputActionButton, OutputTextVerbatim +from playwright.sync_api import Page + + +def test_accordion(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + acc = Accordion(page, "acc") + acc_panel_A = acc.accordion_panel("Section A") + output_txt_verbatim = OutputTextVerbatim(page, "acc_txt") + alternate_button = InputActionButton(page, "alternate") + open_all_button = InputActionButton(page, "open_all") + close_all_button = InputActionButton(page, "close_all") + toggle_b_button = InputActionButton(page, "toggle_b") + toggle_updates_button = InputActionButton(page, "toggle_updates") + toggle_efg_button = InputActionButton(page, "toggle_efg") + acc.expect_width(None) + acc.expect_height(None) + + # initial state - by default only A is open + acc.expect_panels(["Section A", "Section B", "Section C", "Section D"]) + output_txt_verbatim.expect_value("input.acc(): ('Section A',)") + acc.expect_open(["Section A"]) + acc_panel_A.expect_label("Section A") + acc_panel_A.expect_body("Some narrative for section A") + acc_panel_A.expect_open(True) + + alternate_button.click() + acc.expect_open(["Section B", "Section D"]) + output_txt_verbatim.expect_value("input.acc(): ('Section B', 'Section D')") + + alternate_button.click() + acc.expect_open(["Section A", "Section C"]) + output_txt_verbatim.expect_value("input.acc(): ('Section A', 'Section C')") + + open_all_button.click() + acc.expect_open(["Section A", "Section B", "Section C", "Section D"]) + output_txt_verbatim.expect_value( + "input.acc(): ('Section A', 'Section B', 'Section C', 'Section D')" + ) + + close_all_button.click() + acc.expect_open([]) + output_txt_verbatim.expect_value("input.acc(): None") + + toggle_b_button.click() + acc.expect_open(["Section B"]) + output_txt_verbatim.expect_value("input.acc(): ('Section B',)") + + acc_panel_updated_A = acc.accordion_panel("updated_section_a") + toggle_updates_button.click() + acc_panel_updated_A.expect_label("Updated title") + acc_panel_updated_A.expect_body("Updated body") + acc_panel_updated_A.expect_icon("Look! An icon! -->") + + acc.expect_panels(["updated_section_a", "Section B", "Section C", "Section D"]) + output_txt_verbatim.expect_value("input.acc(): ('updated_section_a', 'Section B')") + + toggle_efg_button.click() + acc.expect_panels( + [ + "updated_section_a", + "Section B", + "Section C", + "Section D", + "Section E", + "Section F", + "Section G", + ] + ) + acc.expect_open( + ["updated_section_a", "Section B", "Section E", "Section F", "Section G"] + ) + # will be uncommented once https://github.com/rstudio/bslib/issues/565 is fixed + # output_txt_verbatim.expect_value( + # "input.acc(): ('updated_section_a', 'Section B', 'Section E', 'Section F', 'Section G')" + # ) diff --git a/e2e/experimental/test_autoresize.py b/e2e/experimental/test_autoresize.py new file mode 100644 index 000000000..5c08142a9 --- /dev/null +++ b/e2e/experimental/test_autoresize.py @@ -0,0 +1,34 @@ +from conftest import ShinyAppProc, x_create_doc_example_fixture +from controls import InputTextArea, OutputTextVerbatim +from playwright.sync_api import Locator, Page + +app = x_create_doc_example_fixture("input_text_area") + +resize_number = 6 + + +def get_box_height(locator: Locator) -> float: + bounding_box = locator.bounding_box() + if bounding_box is not None: + return bounding_box["height"] + else: + return 0 + + +def test_autoresize(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + input_area = InputTextArea(page, "caption") + output_txt_verbatim = OutputTextVerbatim(page, "value") + input_area.expect_height(None) + input_area.expect_width(None) + input_area.set("test value") + # use bounding box approach since height is dynamic + initial_height = get_box_height(input_area.loc) + output_txt_verbatim.expect_value("test value") + for _ in range(resize_number): + input_area.loc.press("Enter") + input_area.loc.type("end value") + return_txt = "\n" * resize_number + output_txt_verbatim.expect_value(f"test value{return_txt}end value") + assert get_box_height(input_area.loc) > initial_height