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") diff --git a/shiny/playwright/controller/_controls.py b/shiny/playwright/controller/_controls.py index dd0a426f1..5a69cf36e 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 6c44b508b..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-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 import great_tables as gt 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..c11836c51 --- /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}")) # pyright: 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..14dfcc545 --- /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) + + # 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) + + # 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) + + # 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)