Skip to content

Commit 7cd9e5e

Browse files
tests: Add dataframe filtering and sorting tests (#1369)
Co-authored-by: Barret Schloerke <barret@posit.co>
1 parent eb65d62 commit 7cd9e5e

File tree

7 files changed

+315
-16
lines changed

7 files changed

+315
-16
lines changed

tests/playwright/controls.py

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import pathlib
7+
import platform
78
import re
89
import sys
910
import time
@@ -3566,7 +3567,18 @@ def __init__(self, page: Page, id: str) -> None:
35663567
self.loc_column_label = self.loc_head.locator("> tr > th:not(.filters th)")
35673568

35683569
def cell_locator(self, row: int, col: int) -> Locator:
3569-
return self.loc_body.locator(f"> tr:nth-child({row + 1})").locator(
3570+
"""
3571+
Returns the locator for a specific cell in the data frame.
3572+
3573+
Parameters
3574+
----------
3575+
row
3576+
The row number of the cell.
3577+
col
3578+
The column number of the cell.
3579+
"""
3580+
return self.loc_body.locator(f"> tr[data-index='{row}']").locator(
3581+
# nth-child starts from index = 1
35703582
f"> td:nth-child({col + 1}), > th:nth-child({col + 1})"
35713583
)
35723584

@@ -3609,6 +3621,7 @@ def expect_cell(
36093621
"""
36103622
assert_type(row, int)
36113623
assert_type(col, int)
3624+
self._cell_scroll_if_needed(row=row, col=col, timeout=timeout)
36123625
playwright_expect(self.cell_locator(row, col)).to_have_text(
36133626
text, timeout=timeout
36143627
)
@@ -3642,6 +3655,47 @@ def expect_column_labels(
36423655
labels, timeout=timeout
36433656
)
36443657

3658+
def _cell_scroll_if_needed(self, *, row: int, col: int, timeout: Timeout):
3659+
"""
3660+
Scrolls the cell into view if needed.
3661+
3662+
Parameters
3663+
----------
3664+
row
3665+
The row number of the cell.
3666+
col
3667+
The column number of the cell.
3668+
timeout
3669+
The maximum time to wait for the action to complete.
3670+
"""
3671+
# Check first and last row data-index and make sure `row` is included
3672+
3673+
cell = self.cell_locator(row=row, col=col)
3674+
3675+
# Scroll down if top number is larger
3676+
while not cell.is_visible(timeout=timeout):
3677+
first_row = self.loc_body.locator("> tr[data-index]").first
3678+
first_row_index = first_row.get_attribute("data-index")
3679+
if first_row_index is None:
3680+
break
3681+
if int(first_row_index) >= row:
3682+
first_row.scroll_into_view_if_needed(timeout=timeout)
3683+
else:
3684+
# First row index is lower than `row`
3685+
break
3686+
# Scroll up if bottom number is smaller
3687+
while not cell.is_visible(timeout=timeout):
3688+
last_row = self.loc_body.locator("> tr[data-index]").last
3689+
last_row_index = last_row.get_attribute("data-index")
3690+
if last_row_index is None:
3691+
break
3692+
if int(last_row_index) <= row:
3693+
last_row.scroll_into_view_if_needed(timeout=timeout)
3694+
else:
3695+
# Last row index is higher than `row`
3696+
break
3697+
cell.scroll_into_view_if_needed(timeout=timeout)
3698+
36453699
def expect_column_label(
36463700
self,
36473701
text: ListPatternOrStr,
@@ -3717,6 +3771,46 @@ def expect_cell_class(
37173771
timeout=timeout,
37183772
)
37193773

3774+
def select_rows(
3775+
self,
3776+
rows: list[int],
3777+
*,
3778+
timeout: Timeout = None,
3779+
) -> None:
3780+
"""
3781+
Selects the rows in the data frame.
3782+
3783+
Parameters
3784+
----------
3785+
rows
3786+
The list of row numbers to select.
3787+
timeout
3788+
The maximum time to wait for the action to complete. Defaults to None.
3789+
"""
3790+
if len(rows) > 1:
3791+
rows = sorted(rows)
3792+
# check if the items in the row contain all numbers from index 0 to index -1
3793+
if rows == list(range(rows[0], rows[-1] + 1)):
3794+
self.page.keyboard.down("Shift")
3795+
self.cell_locator(row=rows[0], col=0).click(timeout=timeout)
3796+
self.cell_locator(row=rows[-1], col=0).click(timeout=timeout)
3797+
self.page.keyboard.up("Shift")
3798+
else:
3799+
# if operating system is MacOs use Meta (Cmd) else use Ctrl key
3800+
if platform.system() == "Darwin":
3801+
self.page.keyboard.down("Meta")
3802+
else:
3803+
self.page.keyboard.down("Control")
3804+
for row in rows:
3805+
self._cell_scroll_if_needed(row=row, col=0, timeout=timeout)
3806+
self.cell_locator(row=row, col=0).click(timeout=timeout)
3807+
if platform.system() == "Darwin":
3808+
self.page.keyboard.up("Meta")
3809+
else:
3810+
self.page.keyboard.up("Control")
3811+
else:
3812+
self.cell_locator(row=rows[0], col=0).click(timeout=timeout)
3813+
37203814
def expect_class_state(
37213815
self,
37223816
state: str,
@@ -3768,8 +3862,9 @@ def edit_cell(
37683862
The maximum time to wait for the action to complete. Defaults to None.
37693863
"""
37703864
cell = self.cell_locator(row=row, col=col)
3771-
cell.scroll_into_view_if_needed(timeout=timeout)
3772-
cell.click()
3865+
3866+
self._cell_scroll_if_needed(row=row, col=col, timeout=timeout)
3867+
cell.click(timeout=timeout)
37733868
cell.locator("> textarea").fill(text)
37743869

37753870
def set_column_sort(
@@ -3874,6 +3969,7 @@ def expect_cell_title(
38743969
timeout
38753970
The maximum time to wait for the expectation to pass. Defaults to None.
38763971
"""
3972+
38773973
playwright_expect(self.cell_locator(row=row, col=col)).to_have_attribute(
38783974
name="title", value=message, timeout=timeout
38793975
)

tests/playwright/shiny/components/data_frame/edit/app.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,6 @@
1414
# TODO-future; Can we maintain pre-processed value and use it within editing?
1515
# A: Doesn't seem possible for now
1616

17-
# TODO-karan-test; Edit a cell in the first row and hit `shift+enter`. It should not submit the change and stay editing the current cell
18-
# TODO-karan-test; Edit a cell in the last row and hit `enter`. It should not submit the change and stay editing the current cell
19-
20-
# TODO-karan-test; Data frame with html content in the first two columns; Edit a cell in the third column and try to hit `shift + tab`. It should not submit the edit in the current cell and stay at the current cell (not moving to the second or first column)
21-
# TODO-karan-test; Data frame with html content in the last two columns; Edit a cell in the third from last column and try to hit `tab`. It should not submit the edit in the current cell and stay at the current cell (not moving to the last two columns)
22-
23-
# TODO-karan-test; The resulting data frame `._input_column_sort()` should return the columns that was sorted on and their direction. (Is multi sort allowed?)
24-
# TODO-karan-test; The resulting data frame `._input_column_filter()` should return the columns that was filtered on and their filter values. (Test both string and number columns)
25-
# TODO-karan-test; The resulting data frame `._input_data_view_indices()` should return the start and end index of the data view. (Test with and without filters and sorting)
26-
# TODO-karan-test; The resulting data frame `data_view(selected=False)` should return the data view that is currently being displayed. (Test with and without filters and sorting)
27-
# TODO-karan-test; The resulting data frame `data_view(selected=True)` should return the data view that is currently being displayed, but only the selected rows. (Test with and without filters and sorting)
28-
# TODO-karan-test; The resulting data frame `input_cell_selection()` should return the currently selected cells.
29-
3017
# Load the dataset
3118
penguins = load_penguins_raw()
3219
df = penguins
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import pandas as pd
2+
import seaborn as sns
3+
4+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
5+
6+
df = pd.DataFrame(
7+
sns.load_dataset( # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
8+
"iris"
9+
) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
10+
)
11+
12+
distinct_df = df.drop_duplicates(subset=["species"])
13+
app_ui = ui.page_fluid(
14+
ui.row(
15+
ui.column(
16+
6,
17+
ui.h2("Iris Dataset"),
18+
),
19+
ui.column(2, ui.input_action_button("reset_df", "Reset Dataframe")),
20+
),
21+
ui.output_data_frame("iris_df"),
22+
ui.h2("Data view indices"),
23+
ui.output_text_verbatim("data_view_indices"),
24+
ui.h2("Indices when view_selected=True"),
25+
ui.output_text_verbatim("data_view_selected_true"),
26+
ui.h2("Indices when view_selected=False"),
27+
ui.output_text_verbatim("data_view_selected_false"),
28+
ui.h2("Show selected cell"),
29+
ui.output_text_verbatim("cell_selection"),
30+
)
31+
32+
33+
def server(input: Inputs, output: Outputs, session: Session) -> None:
34+
35+
@render.data_frame
36+
def iris_df():
37+
return render.DataGrid(
38+
data=distinct_df, # pyright: ignore[reportUnknownArgumentType]
39+
filters=True,
40+
selection_mode="rows",
41+
)
42+
43+
@render.code # pyright: ignore[reportArgumentType]
44+
def data_view_indices():
45+
return iris_df._input_data_view_indices()
46+
47+
@render.code # pyright: ignore[reportArgumentType]
48+
def data_view_selected_false(): # pyright: ignore[reportUnknownParameterType]
49+
return iris_df.data_view(
50+
selected=False
51+
).index.values # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
52+
53+
@render.code # pyright: ignore[reportArgumentType]
54+
def data_view_selected_true(): # pyright: ignore[reportUnknownParameterType]
55+
return iris_df.data_view(
56+
selected=True
57+
).index.values # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
58+
59+
@render.code # pyright: ignore[reportArgumentType]
60+
def cell_selection(): # pyright: ignore[reportUnknownParameterType]
61+
return iris_df.input_cell_selection()["rows"] # pyright: ignore
62+
63+
@reactive.Effect
64+
@reactive.event(input.reset_df)
65+
def reset_df():
66+
iris_df._reset_reactives()
67+
68+
69+
app = App(app_ui, server)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from conftest import ShinyAppProc
2+
from controls import InputActionButton, OutputCode, OutputDataFrame
3+
from playwright.sync_api import Page
4+
5+
6+
def test_dataframe_organization_methods(page: Page, local_app: ShinyAppProc) -> None:
7+
page.goto(local_app.url)
8+
data_frame = OutputDataFrame(page, "iris_df")
9+
input_view_indices = OutputCode(page, "data_view_indices")
10+
input_view_selected_true = OutputCode(page, "data_view_selected_true")
11+
input_view_selected_false = OutputCode(page, "data_view_selected_false")
12+
input_cell_selection = OutputCode(page, "cell_selection")
13+
reset_df = InputActionButton(page, "reset_df")
14+
15+
# assert value of unsorted table
16+
input_view_indices.expect_value("(0, 1, 2)")
17+
input_view_selected_true.expect_value("[]")
18+
input_view_selected_false.expect_value("[ 0 50 100]")
19+
input_cell_selection.expect_value("()")
20+
21+
# sort column by number descending
22+
data_frame.set_column_sort(col=0)
23+
input_view_indices.expect_value("(1, 2, 0)")
24+
input_view_selected_true.expect_value("[]")
25+
input_view_selected_false.expect_value("[ 50 100 0]")
26+
input_cell_selection.expect_value("()")
27+
28+
# sort column by number ascending
29+
data_frame.set_column_sort(col=0)
30+
input_view_indices.expect_value("(0, 2, 1)")
31+
input_view_selected_true.expect_value("[]")
32+
input_view_selected_false.expect_value("[ 0 100 50]")
33+
input_cell_selection.expect_value("()")
34+
35+
# sort column by text ascending
36+
data_frame.set_column_sort(col=4)
37+
input_view_indices.expect_value("(0, 1, 2)")
38+
input_view_selected_true.expect_value("[]")
39+
input_view_selected_false.expect_value("[ 0 50 100]")
40+
input_cell_selection.expect_value("()")
41+
42+
# sort column by text descending
43+
data_frame.set_column_sort(col=4)
44+
input_view_indices.expect_value("(2, 1, 0)")
45+
input_view_selected_true.expect_value("[]")
46+
input_view_selected_false.expect_value("[100 50 0]")
47+
input_cell_selection.expect_value("()")
48+
49+
# reset dataframe
50+
reset_df.click()
51+
52+
# filter using numbers
53+
data_frame.set_column_filter(col=0, text=["6", "7"])
54+
input_view_indices.expect_value("(1, 2)")
55+
input_view_selected_true.expect_value("[]")
56+
input_view_selected_false.expect_value("[ 50 100]")
57+
input_cell_selection.expect_value("()")
58+
59+
# reset dataframe
60+
reset_df.click()
61+
62+
# select multiple rows
63+
data_frame.select_rows([0, 2])
64+
input_view_indices.expect_value("(0, 1, 2)")
65+
input_view_selected_true.expect_value("[ 0 100]")
66+
input_view_selected_false.expect_value("[ 0 50 100]")
67+
input_cell_selection.expect_value("(0, 2)")
68+
69+
# reset dataframe
70+
reset_df.click()
71+
72+
# select single row
73+
data_frame.select_rows([0])
74+
input_view_indices.expect_value("(0, 1, 2)")
75+
input_view_selected_true.expect_value("[0]")
76+
input_view_selected_false.expect_value("[ 0 50 100]")
77+
input_cell_selection.expect_value("(0,)")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pandas as pd
2+
import seaborn as sns
3+
4+
from shiny import App, Inputs, Outputs, Session, render, ui
5+
6+
df = pd.DataFrame(
7+
sns.load_dataset(
8+
"iris"
9+
) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
10+
)
11+
app_ui = ui.page_fluid(
12+
ui.h2("Iris Dataset"),
13+
ui.output_data_frame("iris_df"),
14+
)
15+
16+
df["sepal_length"] = df["sepal_length"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
17+
df["sepal_width"] = df["sepal_width"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
18+
df["petal_width"] = df["petal_width"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
19+
df["species"] = df["species"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
20+
21+
22+
def server(input: Inputs, output: Outputs, session: Session) -> None:
23+
24+
@render.data_frame
25+
def iris_df():
26+
return render.DataGrid(
27+
data=df.head(), # pyright: ignore[reportUnknownArgumentType]
28+
editable=True,
29+
)
30+
31+
32+
app = App(app_ui, server)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from conftest import ShinyAppProc
2+
from controls import OutputDataFrame
3+
from playwright.sync_api import Page
4+
5+
6+
def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None:
7+
page.goto(local_app.url)
8+
9+
data_frame = OutputDataFrame(page, "iris_df")
10+
# Data frame with html content in the first two columns; Edit a cell in the third column and try to hit `shift + tab`. It should not submit the edit in the current cell and stay at the current cell (not moving to the second or first column)
11+
data_frame.expect_cell("1.4", row=0, col=2)
12+
data_frame.save_cell("152", row=0, col=2, save_key="Shift+Tab")
13+
data_frame.expect_cell("1.4", row=0, col=2)
14+
data_frame.expect_cell_class("cell-edit-editing", row=0, col=2)
15+
16+
# Data frame with html content in the last two columns; Edit a cell in the third from last column and try to hit `tab`. It should not submit the edit in the current cell and stay at the current cell (not moving to the last two columns)
17+
data_frame.expect_cell("1.4", row=0, col=2)
18+
data_frame.save_cell("152", row=0, col=2, save_key="Tab")
19+
data_frame.expect_cell("1.4", row=0, col=2)
20+
data_frame.expect_cell_class("cell-edit-editing", row=0, col=2)

tests/playwright/shiny/components/data_frame/html_columns_df/test_html_columns.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,21 @@ def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None:
6363
# Filter using a range for a column that contains numbers
6464
data_frame.set_column_filter(1, text=["40", "50"])
6565
data_frame.expect_cell("40", row=0, col=1)
66+
67+
# Editing a cell in the first row and hitting `shift+enter` should not submit the change and stay editing the current cell
68+
data_frame.expect_cell("N25A2", row=0, col=6)
69+
data_frame.save_cell("NAAAAA", row=0, col=6, save_key="Shift+Enter")
70+
data_frame.expect_cell("N25A2", row=0, col=6)
71+
data_frame.save_cell("NAAAAA", row=0, col=6, save_key="Escape")
72+
data_frame.expect_cell("N25A2", row=0, col=6)
73+
74+
# Editing a cell in the last row and hitting `enter` should not submit the change and stay editing the current cell
75+
# data_frame.set_column_filter(7, text="No")
76+
# Test scrolling to last row
77+
data_frame.save_cell("NAAAAA", row=7, col=6, save_key="Enter")
78+
data_frame.expect_cell("N29A2", row=7, col=6)
79+
data_frame.save_cell("NAAAAA", row=7, col=6, save_key="Escape")
80+
data_frame.expect_cell("N29A2", row=7, col=6)
81+
82+
# Test scrolling up to top
83+
data_frame.expect_cell("N25A2", row=0, col=6)

0 commit comments

Comments
 (0)