From 3889b683a7c3a6c73124221d9fbfe43ae2bc15dc Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 28 Jun 2024 10:58:51 -0700 Subject: [PATCH 1/4] Add tests for dataframe --- shiny/playwright/controller/_controls.py | 87 +++++++++++++++++++ .../shiny/components/data_frame/edit/app.py | 6 -- .../validate_row_selection_edit_mode/app.py | 26 ++++++ .../test_validate_row_selection_edit_mode.py | 71 +++++++++++++++ 4 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py create mode 100644 tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py diff --git a/shiny/playwright/controller/_controls.py b/shiny/playwright/controller/_controls.py index 53aa60cca..7bf06029d 100644 --- a/shiny/playwright/controller/_controls.py +++ b/shiny/playwright/controller/_controls.py @@ -5747,6 +5747,7 @@ def cell_locator(self, row: int, col: int) -> Locator: .nth(col) ) + # TODO-barret; Should this be called `expect_row_count()`? def expect_n_row(self, value: int, *, timeout: Timeout = None): """ Expects the number of rows in the data frame. @@ -5762,6 +5763,92 @@ def expect_n_row(self, value: int, *, timeout: Timeout = None): value, timeout=timeout ) + def expect_selected_n_row(self, value: int, *, timeout: Timeout = None): + """ + Expects the number of selected rows in the data frame. + + Parameters + ---------- + value + The expected number of selected rows. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + playwright_expect( + self.loc_body.locator("tr[aria-selected=true]") + ).to_have_count(value, timeout=timeout) + + def expect_selected_rows(self, rows: list[int], *, timeout: Timeout = None): + """ + Expects the specified rows to be selected. + + Parameters + ---------- + rows + The row numbers. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + # * given container... + # * Add that container has all known rows + # * Verify that selected row count is of size N + big_loc = self.loc_body + assert len(rows) > 0 + for row in rows: + big_loc = big_loc.locator( + "xpath=.", # return "self" + has=self.page.locator( + f"> tr[data-index='{row}'][aria-selected='true']" + ), + ) + + try: + playwright_expect( + big_loc.locator("> tr[aria-selected='true']") + ).to_have_count(len(rows), timeout=timeout) + except AssertionError as e: + # Debug expections + + # Expecting container to exist (count = 1) + playwright_expect(self.loc_body).to_have_count(1, timeout=timeout) + + for row in rows: + # Expecting item `{item}` to exist in container + # Perform exact matches on strings. + playwright_expect( + # Simple approach as position is not needed + self.loc_body.locator( + f"> tr[aria-selected='true'][data-index='{row}']", + ) + ).to_have_count(1, timeout=timeout) + + # Could not find the reason why. Raising the original error. + raise e + + def expect_row_focus_state( + self, in_focus: bool = True, *, row: int, timeout: Timeout = None + ): + """ + Expects the focus state of the specified row. + + Parameters + ---------- + row + The row number. + in_focus + `True` if the row is expected to be in focus, `False` otherwise. + timeout + The maximum time to wait for the expectation to pass. Defaults to `None`. + """ + if in_focus: + playwright_expect( + self.loc_body.locator(f"> tr[data-index='{row}']") + ).to_be_focused(timeout=timeout) + else: + playwright_expect( + self.loc_body.locator(f"> tr[data-index='{row}']") + ).not_to_be_focused(timeout=timeout) + def expect_cell( self, value: PatternOrStr, diff --git a/tests/playwright/shiny/components/data_frame/edit/app.py b/tests/playwright/shiny/components/data_frame/edit/app.py index 415cc1459..58161070e 100644 --- a/tests/playwright/shiny/components/data_frame/edit/app.py +++ b/tests/playwright/shiny/components/data_frame/edit/app.py @@ -11,12 +11,6 @@ # TODO-barret-render.data_frame; Make an example that uses a dataframe that then updates a higher level reactive, that causes the df to update... which causes the table to render completely # TODO-barret-render.data_frame; When "updating" data, try to maintain the scroll, filter info when a new `df` is supplied; -# TODO-karan-test; Click outside the table. Tab to the column name, hit enter. Verify the table becomes sorted. Tab to an HTML column name, hit enter. Verify the sort does not update. - -# TODO-karan-test; Enable rows selection and editable. Select (and verify) a row. Edit a cell content in that row. Verify the row is not focused. Hit escape key. Verify the cell value is not updated. Verify the row is focused. Hit escape key again. Verify the row is not focused. (Possibly verify the container div is focused?) -# TODO-karan-test; Enable rows selection and editable. Select (and verify) a row. Edit a cell content in that row. Click a cell in another row. Verify the new row is selected and focused. Verify the old row is not selected. Verify the old row cell value was updated. -# TODO-karan-test; Enable rows selection and editable. Select (and verify) a row. Hit enter to edit the first cell in that row. Hit escape key. Verify the same row is focused. Scroll right and display an html column in the left part of the view. Hit enter to edit the first visible non-html cell in that row. Verify that cell is editing. - # TODO-future; Can we maintain pre-processed value and use it within editing? # A: Doesn't seem possible for now diff --git a/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py new file mode 100644 index 000000000..a65ffac8b --- /dev/null +++ b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py @@ -0,0 +1,26 @@ +from palmerpenguins import load_penguins_raw # pyright: ignore[reportMissingTypeStubs] + +from shiny import App, Inputs, Outputs, Session, render, ui + +df = load_penguins_raw() + +df["Species"] = df["Species"].apply(lambda x: ui.HTML(f"{x}")) # type: ignore + + +app_ui = ui.page_fluid( + ui.h2("Palmer Penguins"), + ui.output_data_frame("penguins_df"), +) + + +def server(input: Inputs, output: Outputs, session: Session) -> None: + @render.data_frame + def penguins_df(): + return render.DataGrid( + data=df, # pyright: ignore[reportUnknownArgumentType] + editable=True, + selection_mode="rows", + ) + + +app = App(app_ui, server) diff --git a/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py new file mode 100644 index 000000000..f981f9603 --- /dev/null +++ b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py @@ -0,0 +1,71 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.run import ShinyAppProc + + +def test_validate_row_selection_in_edit_mode( + page: Page, local_app: ShinyAppProc +) -> None: + page.goto(local_app.url) + + # Select (and verify) a row. Edit a cell content in that row. + # Verify the row is not focused. Hit escape key. Verify the cell value is not updated. + # Verify the row is focused. Hit escape key again. + # Verify the row is not focused. (Possibly verify the container div is focused?) + data_frame = controller.OutputDataFrame(page, "penguins_df") + + data_frame.expect_cell("N1A2", row=1, col=6) + data_frame.edit_cell("N2A2", row=1, col=6) + data_frame.expect_row_focus_state(False, row=1) + data_frame.expect_class_state("editing", row=1, col=6) + data_frame.expect_selected_n_row(1) + data_frame.expect_selected_rows([1]) + data_frame.save_cell("N3A2", row=1, col=6, save_key="Escape") + data_frame.expect_cell("N1A2", row=1, col=6) + data_frame.expect_row_focus_state(True, row=1) + page.keyboard.press("Escape") + data_frame.expect_row_focus_state(False, row=1) + + # TODO-karan-test; Enable rows selection and editable. + # Select (and verify) a row. Edit a cell content in that row. + # Click a cell in another row. Verify the new row is selected and focused. + # Verify the old row is not selected. Verify the old row cell value was updated. + data_frame.expect_cell("N1A2", row=1, col=6) + data_frame.edit_cell("N2A2", row=1, col=6) + data_frame.expect_row_focus_state(False, row=1) + data_frame.expect_class_state("editing", row=1, col=6) + data_frame.cell_locator(row=2, col=6).click() + data_frame.expect_row_focus_state(True, row=2) + data_frame.expect_row_focus_state(False, row=1) + data_frame.expect_cell("N2A2", row=1, col=6) + + # TODO-karan-test; Enable rows selection and editable. + # Select (and verify) a row. Hit enter to edit the first cell in that row. + # Hit escape key. Verify the same row is focused. + # Scroll right and display an html column in the left part of the view. + # Hit enter to edit the first visible non-html cell in that row. + # Verify that cell is editing. + data_frame.cell_locator(row=1, col=2).click() + page.keyboard.press("Enter") + data_frame.expect_row_focus_state(False, row=1) + page.keyboard.press("Escape") + data_frame.expect_row_focus_state(True, row=1) + page.keyboard.press("Escape") + data_frame.edit_cell("Temp value", row=1, col=16) + page.keyboard.press("Escape") + page.keyboard.press("Enter") + data_frame.expect_class_state("editing", row=1, col=0) + + # TODO-karan-test; Click outside the table/Press Escape to exit row focus. + # Tab to the column name, hit enter. Verify the table becomes sorted. + # Tab to an HTML column name, hit enter. Verify the sort does not update. + page.keyboard.press("Escape") + page.keyboard.press("Escape") + page.keyboard.press("Tab") + page.keyboard.press("Tab") # tab to sample number + page.keyboard.press("Enter") + data_frame.expect_cell("152", row=0, col=1) + page.keyboard.press("Tab") + page.keyboard.press("Enter") + data_frame.expect_cell("Adelie Penguin (Pygoscelis adeliae)", row=0, col=2) From 11990d84229d410e4df2dfb0fa5f3b9fcd71ca5a Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Sat, 29 Jun 2024 21:32:36 -0700 Subject: [PATCH 2/4] add additional instructions after shiny add test --- shiny/_template_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shiny/_template_utils.py b/shiny/_template_utils.py index 31189fe94..c870cd353 100644 --- a/shiny/_template_utils.py +++ b/shiny/_template_utils.py @@ -430,3 +430,7 @@ def {test_name}(page: Page, app: ShinyAppProc): # Write template to test file test_file.write_text(template) + + # next steps + print("\nNext steps:") + print("- Run `pytest` in your terminal to run all the tests") From b702d351f95f7c99e1607dcd140b7b78aab05cfe Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 2 Jul 2024 09:58:03 -0400 Subject: [PATCH 3/4] Apply suggestions from code review --- .../data_frame/validate_row_selection_edit_mode/app.py | 2 +- .../test_validate_row_selection_edit_mode.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py index a65ffac8b..c11836c51 100644 --- a/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py +++ b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/app.py @@ -4,7 +4,7 @@ df = load_penguins_raw() -df["Species"] = df["Species"].apply(lambda x: ui.HTML(f"{x}")) # type: ignore +df["Species"] = df["Species"].apply(lambda x: ui.HTML(f"{x}")) # pyright: ignore app_ui = ui.page_fluid( diff --git a/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py index f981f9603..14dfcc545 100644 --- a/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py +++ b/tests/playwright/shiny/components/data_frame/validate_row_selection_edit_mode/test_validate_row_selection_edit_mode.py @@ -27,7 +27,7 @@ def test_validate_row_selection_in_edit_mode( page.keyboard.press("Escape") data_frame.expect_row_focus_state(False, row=1) - # TODO-karan-test; Enable rows selection and editable. + # Enable rows selection and editable. # Select (and verify) a row. Edit a cell content in that row. # Click a cell in another row. Verify the new row is selected and focused. # Verify the old row is not selected. Verify the old row cell value was updated. @@ -40,7 +40,7 @@ def test_validate_row_selection_in_edit_mode( data_frame.expect_row_focus_state(False, row=1) data_frame.expect_cell("N2A2", row=1, col=6) - # TODO-karan-test; Enable rows selection and editable. + # Enable rows selection and editable. # Select (and verify) a row. Hit enter to edit the first cell in that row. # Hit escape key. Verify the same row is focused. # Scroll right and display an html column in the left part of the view. @@ -57,7 +57,7 @@ def test_validate_row_selection_in_edit_mode( page.keyboard.press("Enter") data_frame.expect_class_state("editing", row=1, col=0) - # TODO-karan-test; Click outside the table/Press Escape to exit row focus. + # Click outside the table/Press Escape to exit row focus. # Tab to the column name, hit enter. Verify the table becomes sorted. # Tab to an HTML column name, hit enter. Verify the sort does not update. page.keyboard.press("Escape") From a4e2938c9b0c5947e82afd53cc4a6d949217bf47 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 2 Jul 2024 10:11:22 -0400 Subject: [PATCH 4/4] Update app.py --- tests/playwright/shiny/components/data_frame/edit/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/playwright/shiny/components/data_frame/edit/app.py b/tests/playwright/shiny/components/data_frame/edit/app.py index 0583b7b60..171ae7a77 100644 --- a/tests/playwright/shiny/components/data_frame/edit/app.py +++ b/tests/playwright/shiny/components/data_frame/edit/app.py @@ -4,12 +4,12 @@ # pyright: reportMissingTypeStubs = false # pyright: reportArgumentType = false # pyright: reportUnknownMemberType = false - +# # TODO-barret-render.data_frame; Make an example that uses a dataframe that then updates a higher level reactive, that causes the df to update... which causes the table to render completely # TODO-barret-render.data_frame; When "updating" data, try to maintain the scroll, filter info when a new `df` is supplied; - +# # TODO-karan-test; Click outside the table. Tab to the column name, hit enter. Verify the table becomes sorted. Tab to an HTML column name, hit enter. Verify the sort does not update. - +# # TODO-future; Can we maintain pre-processed value and use it within editing? # A: Doesn't seem possible for now import great_tables as gt