From 9e2fb1f53efdcd32fe32cbd5917247a7a40d5257 Mon Sep 17 00:00:00 2001 From: Karan Date: Tue, 27 Jun 2023 14:23:42 -0700 Subject: [PATCH 01/16] added the first batch of tests --- e2e/controls.py | 85 +++++++++++ e2e/experimental/accordion/app.py | 143 +++++++++++++++++++ e2e/experimental/accordion/test_accordion.py | 24 ++++ 3 files changed, 252 insertions(+) create mode 100644 e2e/experimental/accordion/app.py create mode 100644 e2e/experimental/accordion/test_accordion.py diff --git a/e2e/controls.py b/e2e/controls.py index b19a482ff..debf7ea96 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2337,3 +2337,88 @@ def expect_n_row( n, timeout=timeout, ) + + +### Experimental below + + +class Accordion( + # _SetTextM, + # _ExpectTextInputValueM, + _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", + ), + ) + # self.loc_open = self.loc.locator( + # "> div.accordion-item > div.accordion-collapse.show" + # ) + + # def expect_min( + # self, + # value: AttrValue, + # *, + # timeout: Timeout = None, + # ) -> None: + # expect_attr(self.loc, "min", value=value, timeout=timeout) + + # def expect_max( + # self, + # value: AttrValue, + # *, + # timeout: Timeout = None, + # ) -> None: + # expect_attr(self.loc, "max", value=value, timeout=timeout) + + # def expect_step( + # self, + # value: AttrValue, + # *, + # timeout: Timeout = None, + # ) -> None: + # expect_attr(self.loc, "step", value=value, timeout=timeout) + + 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: + # playwright_expect(self.loc).to_have_attribute(value, timeout=timeout) + playwright_expect(self.loc_open).to_have_count(len(value), timeout=timeout) + + def expect_panels( + self, + value: list[PatternOrStr], + *, + timeout: Timeout = None, + ) -> None: + # playwright_expect(self.loc).to_have_attribute(value, timeout=timeout) + playwright_expect(self.loc).to_have_count(len(value), timeout=timeout) diff --git a/e2e/experimental/accordion/app.py b/e2e/experimental/accordion/app.py new file mode 100644 index 000000000..f55573c9f --- /dev/null +++ b/e2e/experimental/accordion/app.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import shiny.experimental as x +from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui + + +def make_panel(letter: str) -> x.ui.AccordionPanel: + return x.ui.accordion_panel( + f"Section {letter}", + f"Some narrative for section {letter}", + ) + + +items = [make_panel(letter) for letter in "ABCD"] + +accordion = x.ui.accordion(*items, id="acc") +app_ui = ui.page_fluid( + ui.tags.div( + ui.input_action_button("toggle_b", "Open/Close B"), + ui.input_action_button("open_all", "Open All"), + ui.input_action_button("close_all", "Close All"), + ui.input_action_button("alternate", "Alternate"), + ui.input_action_button("toggle_efg", "Add/Remove EFG"), + ui.input_action_button("toggle_updates", "Add/Remove Updates"), + class_="d-flex", + ), + ui.output_text_verbatim("acc_txt", placeholder=True), + accordion, +) + + +def server(input: Inputs, output: Outputs, session: Session) -> None: + @reactive.Calc + def acc() -> list[str]: + acc_val: list[str] | None = input.acc() + if acc_val is None: + acc_val = [] + return acc_val + + @reactive.Effect + def _(): + req(input.toggle_b()) + + with reactive.isolate(): + if "Section B" in acc(): + x.ui.accordion_panel_close("acc", "Section B") + else: + x.ui.accordion_panel_open("acc", "Section B") + + @reactive.Effect + def _(): + req(input.open_all()) + x.ui.accordion_panel_open("acc", True) + + @reactive.Effect + def _(): + req(input.close_all()) + x.ui.accordion_panel_close("acc", True) + + has_efg = False + has_alternate = True + has_updates = False + + @reactive.Effect + def _(): + req(input.alternate()) + + sections = [ + "updated_section_a" if has_updates else "Section A", + "Section B", + "Section C", + "Section D", + ] + if has_efg: + sections.extend(["Section E", "Section F", "Section G"]) + + nonlocal has_alternate + val = int(has_alternate) + sections = [section for i, section in enumerate(sections) if i % 2 == val] + x.ui.accordion_panel_set("acc", sections) + has_alternate = not has_alternate + + @reactive.Effect + def _(): + req(input.toggle_efg()) + + nonlocal has_efg + if has_efg: + x.ui.accordion_panel_remove("acc", ["Section E", "Section F", "Section G"]) + else: + x.ui.accordion_panel_insert("acc", make_panel("E"), "Section D") + x.ui.accordion_panel_insert("acc", make_panel("F"), "Section E") + x.ui.accordion_panel_insert("acc", make_panel("G"), "Section F") + has_efg = not has_efg + + @reactive.Effect + def _(): + req(input.toggle_updates()) + + nonlocal has_updates + if has_updates: + x.ui.update_accordion_panel( + "acc", + "updated_section_a", + "Some narrative for section A", + title="Section A", + value="Section A", + icon="", + ) + else: + with reactive.isolate(): + # print(acc()) + if "Section A" not in acc(): + ui.notification_show("Opening Section A", duration=2) + x.ui.accordion_panel_open("acc", "Section A") + x.ui.update_accordion_panel( + "acc", + "Section A", + "Updated body", + value="updated_section_a", + title=ui.tags.h3("Updated title"), + icon=ui.tags.div( + "Look! An icon! -->", + ui.HTML( + """\ + + + + + """ + ), + ), + ) + + has_updates = not has_updates + + @output + @render.text + def acc_txt(): + return f"input.acc(): {input.acc()}" + + +app = App(app_ui, server) diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py new file mode 100644 index 000000000..9b51872c6 --- /dev/null +++ b/e2e/experimental/accordion/test_accordion.py @@ -0,0 +1,24 @@ +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.expect_width(None) + acc.expect_height(None) + acc.expect_panels(["Section A", "Section B", "Section C", "Section D"]) + OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section A',)") + acc.expect_open(["Section A"]) + + InputActionButton(page, "alternate").click() + acc.expect_open(["Section A", "Section C"]) + # expect(acc.loc_open).to_have_count(2) + OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section B', 'Section D')") + + InputActionButton(page, "alternate").click() + acc.expect_open(["Section B", "Section D"]) + + OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section A', 'Section C')") From 960dcbd4dd0aa1c8da7055ff1f185abbee256df5 Mon Sep 17 00:00:00 2001 From: Karan Date: Thu, 29 Jun 2023 09:48:52 -0700 Subject: [PATCH 02/16] add some more assertions for accordion component --- e2e/controls.py | 49 +++++------ e2e/experimental/accordion/test_accordion.py | 85 +++++++++++++++++++- 2 files changed, 101 insertions(+), 33 deletions(-) diff --git a/e2e/controls.py b/e2e/controls.py index debf7ea96..030a41328 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2371,33 +2371,6 @@ def __init__(self, page: Page, id: str) -> None: "> div.accordion-collapse.show", ), ) - # self.loc_open = self.loc.locator( - # "> div.accordion-item > div.accordion-collapse.show" - # ) - - # def expect_min( - # self, - # value: AttrValue, - # *, - # timeout: Timeout = None, - # ) -> None: - # expect_attr(self.loc, "min", value=value, timeout=timeout) - - # def expect_max( - # self, - # value: AttrValue, - # *, - # timeout: Timeout = None, - # ) -> None: - # expect_attr(self.loc, "max", value=value, timeout=timeout) - - # def expect_step( - # self, - # value: AttrValue, - # *, - # timeout: Timeout = None, - # ) -> None: - # expect_attr(self.loc, "step", value=value, timeout=timeout) def expect_height(self, value: StyleValue, *, timeout: Timeout = None) -> None: expect_to_have_style(self.loc_container, "height", value, timeout=timeout) @@ -2411,7 +2384,6 @@ def expect_open( *, timeout: Timeout = None, ) -> None: - # playwright_expect(self.loc).to_have_attribute(value, timeout=timeout) playwright_expect(self.loc_open).to_have_count(len(value), timeout=timeout) def expect_panels( @@ -2420,5 +2392,24 @@ def expect_panels( *, timeout: Timeout = None, ) -> None: - # playwright_expect(self.loc).to_have_attribute(value, timeout=timeout) playwright_expect(self.loc).to_have_count(len(value), timeout=timeout) + + def expect_all_panels_to_have_attribute( + self, + attribute: str, + value: list[PatternOrStr], + *, + timeout: Timeout = None, + ) -> None: + for index, element in enumerate(value): + playwright_expect(self.loc.nth(index)).to_have_attribute( + attribute, element, timeout=timeout + ) + + def expect_open_panels_to_contain_text( + self, + value: list[PatternOrStr], + *, + timeout: Timeout = None, + ) -> None: + playwright_expect(self.loc_open).to_contain_text(value, timeout=timeout) diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py index 9b51872c6..139b66b53 100644 --- a/e2e/experimental/accordion/test_accordion.py +++ b/e2e/experimental/accordion/test_accordion.py @@ -9,16 +9,93 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: acc = Accordion(page, "acc") 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"]) OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section A',)") acc.expect_open(["Section A"]) + # click on alternate button InputActionButton(page, "alternate").click() - acc.expect_open(["Section A", "Section C"]) + acc.expect_open(["Section B", "Section D"]) + acc.expect_open_panels_to_contain_text( + ["Some narrative for section B", "Some narrative for section D"] + ) # expect(acc.loc_open).to_have_count(2) - OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section B', 'Section D')") + OutputTextVerbatim(page, "acc_txt").expect_value( + "input.acc(): ('Section B', 'Section D')" + ) + # click on alternate once again InputActionButton(page, "alternate").click() - acc.expect_open(["Section B", "Section D"]) + acc.expect_open(["Section A", "Section C"]) + acc.expect_open_panels_to_contain_text( + ["Some narrative for section A", "Some narrative for section C"] + ) + OutputTextVerbatim(page, "acc_txt").expect_value( + "input.acc(): ('Section A', 'Section C')" + ) + + # click on open All + InputActionButton(page, "open_all").click() + acc.expect_open(["Section A", "Section B", "Section C", "Section D"]) + acc.expect_open_panels_to_contain_text( + [ + "Some narrative for section A", + "Some narrative for section B", + "Some narrative for section C", + "Some narrative for section D", + ] + ) + OutputTextVerbatim(page, "acc_txt").expect_value( + "input.acc(): ('Section A', 'Section B', 'Section C', 'Section D')" + ) + + # click on close All + InputActionButton(page, "close_all").click() + acc.expect_open([]) + acc.expect_open_panels_to_contain_text([]) + OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): None") + + # click on open/close B + InputActionButton(page, "toggle_b").click() + acc.expect_open(["Section B"]) + OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section B',)") + + # check attributes for all panels before clicking on add/remove updates + acc.expect_all_panels_to_have_attribute( + "data-value", ["Section A", "Section B", "Section C", "Section D"] + ) + + # check attributes for all panels after clicking on add/remove updates + InputActionButton(page, "toggle_updates").click() + acc.expect_panels(["Section A", "S ection B", "Section C", "Section D"]) + acc.expect_all_panels_to_have_attribute( + "data-value", ["updated_section_a", "Section B", "Section C", "Section D"] + ) + acc.expect_open_panels_to_contain_text( + ["Updated body", "Some narrative for section B"] + ) + OutputTextVerbatim(page, "acc_txt").expect_value( + "input.acc(): ('updated_section_a', 'Section B')" + ) + + # click on add/remove EFG + InputActionButton(page, "toggle_efg").click() + acc.expect_panels( + [ + "Section A", + "Section B", + "Section C", + "Section D", + "Section E", + "Section F", + "Section G", + ] + ) - OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section A', 'Section C')") + acc.expect_open(["Section A", "Section B", "Section E", "Section F", "Section G"]) + # failing since there is a bug in the code + # OutputTextVerbatim(page, "acc_txt").expect_value( + # "input.acc(): ('updated_section_a', 'Section B', 'Section E', 'Section F', 'Section G')" + # ) From 01b536ca0cdf5f06d3f10ab2c8ed588e01f2d965 Mon Sep 17 00:00:00 2001 From: Karan Date: Fri, 7 Jul 2023 10:10:09 -0700 Subject: [PATCH 03/16] Add tests with conventional locators --- e2e/controls.py | 66 +++++++++++++++++-- e2e/experimental/accordion/test_accordion.py | 38 ++++------- e2e/experimental/autoresize/app.py | 17 +++++ .../autoresize/test_autoresize.py | 18 +++++ 4 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 e2e/experimental/autoresize/app.py create mode 100644 e2e/experimental/autoresize/test_autoresize.py diff --git a/e2e/controls.py b/e2e/controls.py index 030a41328..bc8d44cd0 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2097,6 +2097,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) @@ -2401,10 +2402,23 @@ def expect_all_panels_to_have_attribute( *, timeout: Timeout = None, ) -> None: - for index, element in enumerate(value): - playwright_expect(self.loc.nth(index)).to_have_attribute( - attribute, element, timeout=timeout - ) + # page: Page, + # loc_container: Locator, + # el_type: str, + # arr_name: str, + # arr: ListPatternOrStr, + # is_checked: bool | MISSING_TYPE = MISSING, + # timeout: Timeout = None, + # key: str = "value" + _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=attribute, + timeout=timeout, + ) def expect_open_panels_to_contain_text( self, @@ -2413,3 +2427,47 @@ def expect_open_panels_to_contain_text( timeout: Timeout = None, ) -> None: playwright_expect(self.loc_open).to_contain_text(value, timeout=timeout) + + +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") + + self.loc_open = self.loc.locator("> .accordion-collapse.show") + + def expect_label(self, value: PatternOrStr) -> None: + playwright_expect(self.loc_label).to_have_text(value) + + def expect_body(self, value: PatternOrStr) -> None: + playwright_expect(self.loc_body).to_have_text(value) + + def expect_icon(self, value: PatternOrStr) -> None: + playwright_expect(self.loc_icon).to_have_text(value) + + def expect_open(self) -> None: + playwright_expect(self.loc_open).to_have_count(1) diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py index 139b66b53..6cef0a06c 100644 --- a/e2e/experimental/accordion/test_accordion.py +++ b/e2e/experimental/accordion/test_accordion.py @@ -1,5 +1,5 @@ from conftest import ShinyAppProc -from controls import Accordion, InputActionButton, OutputTextVerbatim +from controls import Accordion, AccordionPanel, InputActionButton, OutputTextVerbatim from playwright.sync_api import Page @@ -7,6 +7,7 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) acc = Accordion(page, "acc") + acc_panel_A = AccordionPanel(page, "acc", "Section A") acc.expect_width(None) acc.expect_height(None) @@ -14,14 +15,13 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: acc.expect_panels(["Section A", "Section B", "Section C", "Section D"]) OutputTextVerbatim(page, "acc_txt").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() # click on alternate button InputActionButton(page, "alternate").click() acc.expect_open(["Section B", "Section D"]) - acc.expect_open_panels_to_contain_text( - ["Some narrative for section B", "Some narrative for section D"] - ) - # expect(acc.loc_open).to_have_count(2) OutputTextVerbatim(page, "acc_txt").expect_value( "input.acc(): ('Section B', 'Section D')" ) @@ -29,9 +29,6 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: # click on alternate once again InputActionButton(page, "alternate").click() acc.expect_open(["Section A", "Section C"]) - acc.expect_open_panels_to_contain_text( - ["Some narrative for section A", "Some narrative for section C"] - ) OutputTextVerbatim(page, "acc_txt").expect_value( "input.acc(): ('Section A', 'Section C')" ) @@ -39,14 +36,6 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: # click on open All InputActionButton(page, "open_all").click() acc.expect_open(["Section A", "Section B", "Section C", "Section D"]) - acc.expect_open_panels_to_contain_text( - [ - "Some narrative for section A", - "Some narrative for section B", - "Some narrative for section C", - "Some narrative for section D", - ] - ) OutputTextVerbatim(page, "acc_txt").expect_value( "input.acc(): ('Section A', 'Section B', 'Section C', 'Section D')" ) @@ -66,16 +55,14 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: acc.expect_all_panels_to_have_attribute( "data-value", ["Section A", "Section B", "Section C", "Section D"] ) - + acc_panel_updated_A = AccordionPanel(page, "acc", "updated_section_a") # check attributes for all panels after clicking on add/remove updates InputActionButton(page, "toggle_updates").click() - acc.expect_panels(["Section A", "S ection B", "Section C", "Section D"]) - acc.expect_all_panels_to_have_attribute( - "data-value", ["updated_section_a", "Section B", "Section C", "Section D"] - ) - acc.expect_open_panels_to_contain_text( - ["Updated body", "Some narrative for section B"] - ) + 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(["Section A", "Section B", "Section C", "Section D"]) OutputTextVerbatim(page, "acc_txt").expect_value( "input.acc(): ('updated_section_a', 'Section B')" ) @@ -93,9 +80,8 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: "Section G", ] ) - acc.expect_open(["Section A", "Section B", "Section E", "Section F", "Section G"]) - # failing since there is a bug in the code + # will be uncommented once https://github.com/rstudio/bslib/issues/565 is fixed # OutputTextVerbatim(page, "acc_txt").expect_value( # "input.acc(): ('updated_section_a', 'Section B', 'Section E', 'Section F', 'Section G')" # ) diff --git a/e2e/experimental/autoresize/app.py b/e2e/experimental/autoresize/app.py new file mode 100644 index 000000000..3ddc05523 --- /dev/null +++ b/e2e/experimental/autoresize/app.py @@ -0,0 +1,17 @@ +from shiny import App, Inputs, Outputs, Session, render, ui +import shiny.experimental as x + +app_ui = ui.page_fluid( + x.ui.input_text_area("caption", "Caption:", "Data summary", autoresize=True), + ui.output_text_verbatim("value"), +) + + +def server(input: Inputs, output: Outputs, session: Session): + @output + @render.text + def value(): + return input.caption() + + +app = App(app_ui, server) diff --git a/e2e/experimental/autoresize/test_autoresize.py b/e2e/experimental/autoresize/test_autoresize.py new file mode 100644 index 000000000..174cd57f3 --- /dev/null +++ b/e2e/experimental/autoresize/test_autoresize.py @@ -0,0 +1,18 @@ +from conftest import ShinyAppProc +from controls import InputTextArea, OutputTextVerbatim +from playwright.sync_api import Page + + +def test_autoresize(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + + input_area = InputTextArea(page, "caption") + input_area.expect_height(None) + input_area.expect_width(None) + input_area.set('test value') + OutputTextVerbatim(page, "value").expect_value("test value") + for _ in range(6): + input_area.loc.press("Enter") + # uncomment after bug is fixed - + # input_area.expect_rows("6") + OutputTextVerbatim(page, "value").expect_value("test value") From 829a860284605e36466cddfad1f28f3f7e2e97ff Mon Sep 17 00:00:00 2001 From: Karan Date: Fri, 7 Jul 2023 10:51:39 -0700 Subject: [PATCH 04/16] run formatter on code --- e2e/experimental/autoresize/test_autoresize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/experimental/autoresize/test_autoresize.py b/e2e/experimental/autoresize/test_autoresize.py index 174cd57f3..06b9d229a 100644 --- a/e2e/experimental/autoresize/test_autoresize.py +++ b/e2e/experimental/autoresize/test_autoresize.py @@ -9,10 +9,10 @@ def test_autoresize(page: Page, local_app: ShinyAppProc) -> None: input_area = InputTextArea(page, "caption") input_area.expect_height(None) input_area.expect_width(None) - input_area.set('test value') + input_area.set("test value") OutputTextVerbatim(page, "value").expect_value("test value") for _ in range(6): input_area.loc.press("Enter") - # uncomment after bug is fixed - + # uncomment after bug is fixed - # input_area.expect_rows("6") OutputTextVerbatim(page, "value").expect_value("test value") From a6e037a86109f01cd404ded6e385d307a53e4df6 Mon Sep 17 00:00:00 2001 From: Karan Date: Fri, 7 Jul 2023 11:59:03 -0700 Subject: [PATCH 05/16] Add more fixtures and use test helpers --- e2e/conftest.py | 7 +++++++ e2e/controls.py | 18 ++++++++---------- e2e/experimental/accordion/test_accordion.py | 2 +- e2e/experimental/autoresize/app.py | 17 ----------------- .../{autoresize => }/test_autoresize.py | 11 +++++++---- 5 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 e2e/experimental/autoresize/app.py rename e2e/experimental/{autoresize => }/test_autoresize.py (57%) diff --git a/e2e/conftest.py b/e2e/conftest.py index f8f99e1cd..e23207e00 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -200,6 +200,13 @@ def create_doc_example_fixture(example_name: str, scope: str = "module"): ) +def x_create_doc_example_fixture(example_name: str, scope: str = "module"): + """Used to create app fixtures from apps in py-shiny/shiny/examples""" + return create_app_fixture( + here / "../shiny/experimental/examples" / example_name / "app.py", scope + ) + + @pytest.fixture(scope="module") def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, None]: sa = run_shiny_app(PurePath(request.path).parent / "app.py", wait_for_start=False) diff --git a/e2e/controls.py b/e2e/controls.py index dd8fa1418..650ad401a 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2453,16 +2453,14 @@ def __init__(self, page: Page, id: str, data_value: str) -> None: self.loc_body = self.loc.locator("> .accordion-collapse") - self.loc_open = self.loc.locator("> .accordion-collapse.show") - - def expect_label(self, value: PatternOrStr) -> None: - playwright_expect(self.loc_label).to_have_text(value) + 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) -> None: - playwright_expect(self.loc_body).to_have_text(value) + 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) -> None: - playwright_expect(self.loc_icon).to_have_text(value) + 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) -> None: - playwright_expect(self.loc_open).to_have_count(1) + 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/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py index 6cef0a06c..d023aad4c 100644 --- a/e2e/experimental/accordion/test_accordion.py +++ b/e2e/experimental/accordion/test_accordion.py @@ -17,7 +17,7 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: 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() + acc_panel_A.expect_open(True) # click on alternate button InputActionButton(page, "alternate").click() diff --git a/e2e/experimental/autoresize/app.py b/e2e/experimental/autoresize/app.py deleted file mode 100644 index 3ddc05523..000000000 --- a/e2e/experimental/autoresize/app.py +++ /dev/null @@ -1,17 +0,0 @@ -from shiny import App, Inputs, Outputs, Session, render, ui -import shiny.experimental as x - -app_ui = ui.page_fluid( - x.ui.input_text_area("caption", "Caption:", "Data summary", autoresize=True), - ui.output_text_verbatim("value"), -) - - -def server(input: Inputs, output: Outputs, session: Session): - @output - @render.text - def value(): - return input.caption() - - -app = App(app_ui, server) diff --git a/e2e/experimental/autoresize/test_autoresize.py b/e2e/experimental/test_autoresize.py similarity index 57% rename from e2e/experimental/autoresize/test_autoresize.py rename to e2e/experimental/test_autoresize.py index 06b9d229a..16e8857d6 100644 --- a/e2e/experimental/autoresize/test_autoresize.py +++ b/e2e/experimental/test_autoresize.py @@ -1,10 +1,12 @@ -from conftest import ShinyAppProc +from conftest import ShinyAppProc, x_create_doc_example_fixture from controls import InputTextArea, OutputTextVerbatim from playwright.sync_api import Page +app = x_create_doc_example_fixture("input_text_area") -def test_autoresize(page: Page, local_app: ShinyAppProc) -> None: - page.goto(local_app.url) + +def test_autoresize(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) input_area = InputTextArea(page, "caption") input_area.expect_height(None) @@ -13,6 +15,7 @@ def test_autoresize(page: Page, local_app: ShinyAppProc) -> None: OutputTextVerbatim(page, "value").expect_value("test value") for _ in range(6): input_area.loc.press("Enter") + input_area.loc.type("end value") + OutputTextVerbatim(page, "value").expect_value("test value\n\n\n\n\n\nend value") # uncomment after bug is fixed - # input_area.expect_rows("6") - OutputTextVerbatim(page, "value").expect_value("test value") From dd7830b39a9581bde3e4256fb1953d4c8846452d Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Wed, 12 Jul 2023 22:15:14 -0700 Subject: [PATCH 06/16] Use the expect_height to verify autoresize functionality --- e2e/experimental/test_autoresize.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/e2e/experimental/test_autoresize.py b/e2e/experimental/test_autoresize.py index 16e8857d6..a6f98cdb7 100644 --- a/e2e/experimental/test_autoresize.py +++ b/e2e/experimental/test_autoresize.py @@ -4,6 +4,13 @@ app = x_create_doc_example_fixture("input_text_area") +initial_height = "36px" +resize_number = 6 + + +def resized_height(value: int) -> str: + return str(36 + (24 * value)) + "px" + def test_autoresize(page: Page, app: ShinyAppProc) -> None: page.goto(app.url) @@ -12,10 +19,10 @@ def test_autoresize(page: Page, app: ShinyAppProc) -> None: input_area.expect_height(None) input_area.expect_width(None) input_area.set("test value") + input_area.expect_height(initial_height) OutputTextVerbatim(page, "value").expect_value("test value") - for _ in range(6): + for _ in range(resize_number): input_area.loc.press("Enter") input_area.loc.type("end value") OutputTextVerbatim(page, "value").expect_value("test value\n\n\n\n\n\nend value") - # uncomment after bug is fixed - - # input_area.expect_rows("6") + input_area.expect_height(resized_height(resize_number)) From d8f44dabbc3867d05e10882cf3493c93a4b66173 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 18 Jul 2023 18:44:33 -0700 Subject: [PATCH 07/16] Remove unnecessary comments --- e2e/experimental/accordion/test_accordion.py | 50 +++++++++----------- e2e/experimental/test_autoresize.py | 5 +- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py index d023aad4c..2c0c61b14 100644 --- a/e2e/experimental/accordion/test_accordion.py +++ b/e2e/experimental/accordion/test_accordion.py @@ -8,67 +8,61 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: acc = Accordion(page, "acc") acc_panel_A = AccordionPanel(page, "acc", "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"]) - OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section A',)") + 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) - # click on alternate button - InputActionButton(page, "alternate").click() + alternate_button.click() acc.expect_open(["Section B", "Section D"]) - OutputTextVerbatim(page, "acc_txt").expect_value( - "input.acc(): ('Section B', 'Section D')" - ) + output_txt_verbatim.expect_value("input.acc(): ('Section B', 'Section D')") - # click on alternate once again - InputActionButton(page, "alternate").click() + alternate_button.click() acc.expect_open(["Section A", "Section C"]) - OutputTextVerbatim(page, "acc_txt").expect_value( - "input.acc(): ('Section A', 'Section C')" - ) + output_txt_verbatim.expect_value("input.acc(): ('Section A', 'Section C')") - # click on open All - InputActionButton(page, "open_all").click() + open_all_button.click() acc.expect_open(["Section A", "Section B", "Section C", "Section D"]) - OutputTextVerbatim(page, "acc_txt").expect_value( + output_txt_verbatim.expect_value( "input.acc(): ('Section A', 'Section B', 'Section C', 'Section D')" ) - # click on close All - InputActionButton(page, "close_all").click() + close_all_button.click() acc.expect_open([]) acc.expect_open_panels_to_contain_text([]) - OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): None") + output_txt_verbatim.expect_value("input.acc(): None") - # click on open/close B - InputActionButton(page, "toggle_b").click() + toggle_b_button.click() acc.expect_open(["Section B"]) - OutputTextVerbatim(page, "acc_txt").expect_value("input.acc(): ('Section B',)") + output_txt_verbatim.expect_value("input.acc(): ('Section B',)") # check attributes for all panels before clicking on add/remove updates acc.expect_all_panels_to_have_attribute( "data-value", ["Section A", "Section B", "Section C", "Section D"] ) acc_panel_updated_A = AccordionPanel(page, "acc", "updated_section_a") - # check attributes for all panels after clicking on add/remove updates - InputActionButton(page, "toggle_updates").click() + 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(["Section A", "Section B", "Section C", "Section D"]) - OutputTextVerbatim(page, "acc_txt").expect_value( - "input.acc(): ('updated_section_a', 'Section B')" - ) + output_txt_verbatim.expect_value("input.acc(): ('updated_section_a', 'Section B')") - # click on add/remove EFG - InputActionButton(page, "toggle_efg").click() + toggle_efg_button.click() acc.expect_panels( [ "Section A", @@ -82,6 +76,6 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: ) acc.expect_open(["Section A", "Section B", "Section E", "Section F", "Section G"]) # will be uncommented once https://github.com/rstudio/bslib/issues/565 is fixed - # OutputTextVerbatim(page, "acc_txt").expect_value( + # 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 index a6f98cdb7..e6cc59c7f 100644 --- a/e2e/experimental/test_autoresize.py +++ b/e2e/experimental/test_autoresize.py @@ -16,13 +16,14 @@ 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") input_area.expect_height(initial_height) - OutputTextVerbatim(page, "value").expect_value("test value") + output_txt_verbatim.expect_value("test value") for _ in range(resize_number): input_area.loc.press("Enter") input_area.loc.type("end value") - OutputTextVerbatim(page, "value").expect_value("test value\n\n\n\n\n\nend value") + output_txt_verbatim.expect_value("test value\n\n\n\n\n\nend value") input_area.expect_height(resized_height(resize_number)) From 9b3b43c0d3a9a02848b86a38160e52b4c3b07922 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 18 Jul 2023 19:25:17 -0700 Subject: [PATCH 08/16] change the new changes to example file paths --- e2e/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/conftest.py b/e2e/conftest.py index e23207e00..77b35bb6f 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -203,7 +203,7 @@ def create_doc_example_fixture(example_name: str, scope: str = "module"): def x_create_doc_example_fixture(example_name: str, scope: str = "module"): """Used to create app fixtures from apps in py-shiny/shiny/examples""" return create_app_fixture( - here / "../shiny/experimental/examples" / example_name / "app.py", scope + here / "../shiny/experimental/api-examples" / example_name / "app.py", scope ) From 6eb2d9eba45d665a9f963ef92b353e2a5a45d073 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 18 Jul 2023 20:00:28 -0700 Subject: [PATCH 09/16] use bounding box since hardcoding height was fickle --- e2e/experimental/test_autoresize.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/e2e/experimental/test_autoresize.py b/e2e/experimental/test_autoresize.py index e6cc59c7f..43416ad4f 100644 --- a/e2e/experimental/test_autoresize.py +++ b/e2e/experimental/test_autoresize.py @@ -1,15 +1,18 @@ from conftest import ShinyAppProc, x_create_doc_example_fixture from controls import InputTextArea, OutputTextVerbatim -from playwright.sync_api import Page +from playwright.sync_api import Locator, Page app = x_create_doc_example_fixture("input_text_area") -initial_height = "36px" resize_number = 6 -def resized_height(value: int) -> str: - return str(36 + (24 * value)) + "px" +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: @@ -20,10 +23,11 @@ def test_autoresize(page: Page, app: ShinyAppProc) -> None: input_area.expect_height(None) input_area.expect_width(None) input_area.set("test value") - input_area.expect_height(initial_height) + # 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") output_txt_verbatim.expect_value("test value\n\n\n\n\n\nend value") - input_area.expect_height(resized_height(resize_number)) + assert get_box_height(input_area.loc) > initial_height From 21ffedd5caa1b3438e61aca95af7248ed4f4df5a Mon Sep 17 00:00:00 2001 From: Karan Date: Wed, 19 Jul 2023 16:54:25 -0700 Subject: [PATCH 10/16] Update e2e/controls.py Co-authored-by: Barret Schloerke --- e2e/controls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/e2e/controls.py b/e2e/controls.py index 7769b2bb8..5d58d2612 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2343,8 +2343,6 @@ def expect_n_row( class Accordion( - # _SetTextM, - # _ExpectTextInputValueM, _WidthLocM, _InputWithContainer, ): From 59b9a07433c4f47415739cd25c4c0c8e04b31f88 Mon Sep 17 00:00:00 2001 From: Karan Date: Wed, 19 Jul 2023 16:56:16 -0700 Subject: [PATCH 11/16] Remove all_panels_to_have_attribute Co-authored-by: Barret Schloerke --- e2e/controls.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/e2e/controls.py b/e2e/controls.py index 5d58d2612..a6d3f0c3b 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2392,30 +2392,6 @@ def expect_panels( ) -> None: playwright_expect(self.loc).to_have_count(len(value), timeout=timeout) - def expect_all_panels_to_have_attribute( - self, - attribute: str, - value: list[PatternOrStr], - *, - timeout: Timeout = None, - ) -> None: - # page: Page, - # loc_container: Locator, - # el_type: str, - # arr_name: str, - # arr: ListPatternOrStr, - # is_checked: bool | MISSING_TYPE = MISSING, - # timeout: Timeout = None, - # key: str = "value" - _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=attribute, - timeout=timeout, - ) def expect_open_panels_to_contain_text( self, From c18bc1c032044b7b0cd4be4e8218e5de789fef61 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Thu, 20 Jul 2023 12:55:41 -0700 Subject: [PATCH 12/16] accordion requested changes --- e2e/controls.py | 21 ++- e2e/experimental/accordion/test_accordion.py | 12 +- shiny/experimental/e2e/accordion/app.py | 143 ------------------- 3 files changed, 23 insertions(+), 153 deletions(-) delete mode 100644 shiny/experimental/e2e/accordion/app.py diff --git a/e2e/controls.py b/e2e/controls.py index a6d3f0c3b..178c0d718 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2382,7 +2382,15 @@ def expect_open( *, timeout: Timeout = None, ) -> None: - playwright_expect(self.loc_open).to_have_count(len(value), timeout=timeout) + _MultipleDomItems.expect_locator_values_in_list( + page=self.page, + loc_container=self.loc_container, + el_type="> div.accordion-item:has(> div.accordion-collapse.show)", + arr_name="value", + arr=value, + key="data-value", + timeout=timeout, + ) def expect_panels( self, @@ -2390,8 +2398,15 @@ def expect_panels( *, timeout: Timeout = None, ) -> None: - playwright_expect(self.loc).to_have_count(len(value), timeout=timeout) - + _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 expect_open_panels_to_contain_text( self, diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py index 2c0c61b14..398825000 100644 --- a/e2e/experimental/accordion/test_accordion.py +++ b/e2e/experimental/accordion/test_accordion.py @@ -49,23 +49,19 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: acc.expect_open(["Section B"]) output_txt_verbatim.expect_value("input.acc(): ('Section B',)") - # check attributes for all panels before clicking on add/remove updates - acc.expect_all_panels_to_have_attribute( - "data-value", ["Section A", "Section B", "Section C", "Section D"] - ) acc_panel_updated_A = AccordionPanel(page, "acc", "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(["Section A", "Section B", "Section C", "Section D"]) + 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( [ - "Section A", + "updated_section_a", "Section B", "Section C", "Section D", @@ -74,7 +70,9 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: "Section G", ] ) - acc.expect_open(["Section A", "Section B", "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/shiny/experimental/e2e/accordion/app.py b/shiny/experimental/e2e/accordion/app.py deleted file mode 100644 index f55573c9f..000000000 --- a/shiny/experimental/e2e/accordion/app.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import annotations - -import shiny.experimental as x -from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui - - -def make_panel(letter: str) -> x.ui.AccordionPanel: - return x.ui.accordion_panel( - f"Section {letter}", - f"Some narrative for section {letter}", - ) - - -items = [make_panel(letter) for letter in "ABCD"] - -accordion = x.ui.accordion(*items, id="acc") -app_ui = ui.page_fluid( - ui.tags.div( - ui.input_action_button("toggle_b", "Open/Close B"), - ui.input_action_button("open_all", "Open All"), - ui.input_action_button("close_all", "Close All"), - ui.input_action_button("alternate", "Alternate"), - ui.input_action_button("toggle_efg", "Add/Remove EFG"), - ui.input_action_button("toggle_updates", "Add/Remove Updates"), - class_="d-flex", - ), - ui.output_text_verbatim("acc_txt", placeholder=True), - accordion, -) - - -def server(input: Inputs, output: Outputs, session: Session) -> None: - @reactive.Calc - def acc() -> list[str]: - acc_val: list[str] | None = input.acc() - if acc_val is None: - acc_val = [] - return acc_val - - @reactive.Effect - def _(): - req(input.toggle_b()) - - with reactive.isolate(): - if "Section B" in acc(): - x.ui.accordion_panel_close("acc", "Section B") - else: - x.ui.accordion_panel_open("acc", "Section B") - - @reactive.Effect - def _(): - req(input.open_all()) - x.ui.accordion_panel_open("acc", True) - - @reactive.Effect - def _(): - req(input.close_all()) - x.ui.accordion_panel_close("acc", True) - - has_efg = False - has_alternate = True - has_updates = False - - @reactive.Effect - def _(): - req(input.alternate()) - - sections = [ - "updated_section_a" if has_updates else "Section A", - "Section B", - "Section C", - "Section D", - ] - if has_efg: - sections.extend(["Section E", "Section F", "Section G"]) - - nonlocal has_alternate - val = int(has_alternate) - sections = [section for i, section in enumerate(sections) if i % 2 == val] - x.ui.accordion_panel_set("acc", sections) - has_alternate = not has_alternate - - @reactive.Effect - def _(): - req(input.toggle_efg()) - - nonlocal has_efg - if has_efg: - x.ui.accordion_panel_remove("acc", ["Section E", "Section F", "Section G"]) - else: - x.ui.accordion_panel_insert("acc", make_panel("E"), "Section D") - x.ui.accordion_panel_insert("acc", make_panel("F"), "Section E") - x.ui.accordion_panel_insert("acc", make_panel("G"), "Section F") - has_efg = not has_efg - - @reactive.Effect - def _(): - req(input.toggle_updates()) - - nonlocal has_updates - if has_updates: - x.ui.update_accordion_panel( - "acc", - "updated_section_a", - "Some narrative for section A", - title="Section A", - value="Section A", - icon="", - ) - else: - with reactive.isolate(): - # print(acc()) - if "Section A" not in acc(): - ui.notification_show("Opening Section A", duration=2) - x.ui.accordion_panel_open("acc", "Section A") - x.ui.update_accordion_panel( - "acc", - "Section A", - "Updated body", - value="updated_section_a", - title=ui.tags.h3("Updated title"), - icon=ui.tags.div( - "Look! An icon! -->", - ui.HTML( - """\ - - - - - """ - ), - ), - ) - - has_updates = not has_updates - - @output - @render.text - def acc_txt(): - return f"input.acc(): {input.acc()}" - - -app = App(app_ui, server) From 7b80674f8580505e9d26195b89e84c591240f5d8 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 21 Jul 2023 12:11:50 -0700 Subject: [PATCH 13/16] use accordion panel function instead of class instance --- e2e/controls.py | 10 ++++------ e2e/experimental/accordion/test_accordion.py | 7 +++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/e2e/controls.py b/e2e/controls.py index 5411619a3..f88afb348 100644 --- a/e2e/controls.py +++ b/e2e/controls.py @@ -2637,13 +2637,11 @@ def expect_panels( timeout=timeout, ) - def expect_open_panels_to_contain_text( + def accordion_panel( self, - value: list[PatternOrStr], - *, - timeout: Timeout = None, - ) -> None: - playwright_expect(self.loc_open).to_contain_text(value, timeout=timeout) + data_value: str, + ) -> AccordionPanel: + return AccordionPanel(self.page, self.id, data_value) class AccordionPanel( diff --git a/e2e/experimental/accordion/test_accordion.py b/e2e/experimental/accordion/test_accordion.py index 398825000..330b68544 100644 --- a/e2e/experimental/accordion/test_accordion.py +++ b/e2e/experimental/accordion/test_accordion.py @@ -1,5 +1,5 @@ from conftest import ShinyAppProc -from controls import Accordion, AccordionPanel, InputActionButton, OutputTextVerbatim +from controls import Accordion, InputActionButton, OutputTextVerbatim from playwright.sync_api import Page @@ -7,7 +7,7 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) acc = Accordion(page, "acc") - acc_panel_A = AccordionPanel(page, "acc", "Section A") + 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") @@ -42,14 +42,13 @@ def test_accordion(page: Page, local_app: ShinyAppProc) -> None: close_all_button.click() acc.expect_open([]) - acc.expect_open_panels_to_contain_text([]) 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 = AccordionPanel(page, "acc", "updated_section_a") + 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") From a20ff59860c99cdb758ed95fc5601620c843305f Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 21 Jul 2023 12:34:48 -0700 Subject: [PATCH 14/16] Add support for `expect_locator_values_in_list(el_type)` to be a Locator Co-Authored-By: Barret Schloerke --- e2e/controls.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/e2e/controls.py b/e2e/controls.py index f88afb348..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. @@ -2614,7 +2621,11 @@ def expect_open( _MultipleDomItems.expect_locator_values_in_list( page=self.page, loc_container=self.loc_container, - el_type="> div.accordion-item:has(> div.accordion-collapse.show)", + 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", From c484c289860f0266ce5772b08b5366c6f3bdf179 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 21 Jul 2023 16:00:25 -0400 Subject: [PATCH 15/16] User resize_number to determine `\n` count --- e2e/experimental/test_autoresize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/experimental/test_autoresize.py b/e2e/experimental/test_autoresize.py index 43416ad4f..3e0f43c21 100644 --- a/e2e/experimental/test_autoresize.py +++ b/e2e/experimental/test_autoresize.py @@ -29,5 +29,6 @@ def test_autoresize(page: Page, app: ShinyAppProc) -> None: for _ in range(resize_number): input_area.loc.press("Enter") input_area.loc.type("end value") - output_txt_verbatim.expect_value("test value\n\n\n\n\n\nend 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 From 72e7de36bb9b15e4d6d7aa3dd77875f4c5aee43a Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 21 Jul 2023 13:18:13 -0700 Subject: [PATCH 16/16] address linting issues in test_autoresize file --- e2e/experimental/test_autoresize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/experimental/test_autoresize.py b/e2e/experimental/test_autoresize.py index 3e0f43c21..5c08142a9 100644 --- a/e2e/experimental/test_autoresize.py +++ b/e2e/experimental/test_autoresize.py @@ -29,6 +29,6 @@ def test_autoresize(page: Page, app: ShinyAppProc) -> None: for _ in range(resize_number): input_area.loc.press("Enter") input_area.loc.type("end value") - return_txt="\n" * resize_number + 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