Skip to content

Commit

Permalink
tests: Add dataframe filtering and sorting tests (#1369)
Browse files Browse the repository at this point in the history
Co-authored-by: Barret Schloerke <[email protected]>
  • Loading branch information
karangattu and schloerke authored May 10, 2024
1 parent eb65d62 commit 7cd9e5e
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 16 deletions.
102 changes: 99 additions & 3 deletions tests/playwright/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import pathlib
import platform
import re
import sys
import time
Expand Down Expand Up @@ -3566,7 +3567,18 @@ def __init__(self, page: Page, id: str) -> None:
self.loc_column_label = self.loc_head.locator("> tr > th:not(.filters th)")

def cell_locator(self, row: int, col: int) -> Locator:
return self.loc_body.locator(f"> tr:nth-child({row + 1})").locator(
"""
Returns the locator for a specific cell in the data frame.
Parameters
----------
row
The row number of the cell.
col
The column number of the cell.
"""
return self.loc_body.locator(f"> tr[data-index='{row}']").locator(
# nth-child starts from index = 1
f"> td:nth-child({col + 1}), > th:nth-child({col + 1})"
)

Expand Down Expand Up @@ -3609,6 +3621,7 @@ def expect_cell(
"""
assert_type(row, int)
assert_type(col, int)
self._cell_scroll_if_needed(row=row, col=col, timeout=timeout)
playwright_expect(self.cell_locator(row, col)).to_have_text(
text, timeout=timeout
)
Expand Down Expand Up @@ -3642,6 +3655,47 @@ def expect_column_labels(
labels, timeout=timeout
)

def _cell_scroll_if_needed(self, *, row: int, col: int, timeout: Timeout):
"""
Scrolls the cell into view if needed.
Parameters
----------
row
The row number of the cell.
col
The column number of the cell.
timeout
The maximum time to wait for the action to complete.
"""
# Check first and last row data-index and make sure `row` is included

cell = self.cell_locator(row=row, col=col)

# Scroll down if top number is larger
while not cell.is_visible(timeout=timeout):
first_row = self.loc_body.locator("> tr[data-index]").first
first_row_index = first_row.get_attribute("data-index")
if first_row_index is None:
break
if int(first_row_index) >= row:
first_row.scroll_into_view_if_needed(timeout=timeout)
else:
# First row index is lower than `row`
break
# Scroll up if bottom number is smaller
while not cell.is_visible(timeout=timeout):
last_row = self.loc_body.locator("> tr[data-index]").last
last_row_index = last_row.get_attribute("data-index")
if last_row_index is None:
break
if int(last_row_index) <= row:
last_row.scroll_into_view_if_needed(timeout=timeout)
else:
# Last row index is higher than `row`
break
cell.scroll_into_view_if_needed(timeout=timeout)

def expect_column_label(
self,
text: ListPatternOrStr,
Expand Down Expand Up @@ -3717,6 +3771,46 @@ def expect_cell_class(
timeout=timeout,
)

def select_rows(
self,
rows: list[int],
*,
timeout: Timeout = None,
) -> None:
"""
Selects the rows in the data frame.
Parameters
----------
rows
The list of row numbers to select.
timeout
The maximum time to wait for the action to complete. Defaults to None.
"""
if len(rows) > 1:
rows = sorted(rows)
# check if the items in the row contain all numbers from index 0 to index -1
if rows == list(range(rows[0], rows[-1] + 1)):
self.page.keyboard.down("Shift")
self.cell_locator(row=rows[0], col=0).click(timeout=timeout)
self.cell_locator(row=rows[-1], col=0).click(timeout=timeout)
self.page.keyboard.up("Shift")
else:
# if operating system is MacOs use Meta (Cmd) else use Ctrl key
if platform.system() == "Darwin":
self.page.keyboard.down("Meta")
else:
self.page.keyboard.down("Control")
for row in rows:
self._cell_scroll_if_needed(row=row, col=0, timeout=timeout)
self.cell_locator(row=row, col=0).click(timeout=timeout)
if platform.system() == "Darwin":
self.page.keyboard.up("Meta")
else:
self.page.keyboard.up("Control")
else:
self.cell_locator(row=rows[0], col=0).click(timeout=timeout)

def expect_class_state(
self,
state: str,
Expand Down Expand Up @@ -3768,8 +3862,9 @@ def edit_cell(
The maximum time to wait for the action to complete. Defaults to None.
"""
cell = self.cell_locator(row=row, col=col)
cell.scroll_into_view_if_needed(timeout=timeout)
cell.click()

self._cell_scroll_if_needed(row=row, col=col, timeout=timeout)
cell.click(timeout=timeout)
cell.locator("> textarea").fill(text)

def set_column_sort(
Expand Down Expand Up @@ -3874,6 +3969,7 @@ def expect_cell_title(
timeout
The maximum time to wait for the expectation to pass. Defaults to None.
"""

playwright_expect(self.cell_locator(row=row, col=col)).to_have_attribute(
name="title", value=message, timeout=timeout
)
Expand Down
13 changes: 0 additions & 13 deletions tests/playwright/shiny/components/data_frame/edit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,6 @@
# TODO-future; Can we maintain pre-processed value and use it within editing?
# A: Doesn't seem possible for now

# 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
# 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

# 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)
# 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)

# 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?)
# 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)
# 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)
# 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)
# 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)
# TODO-karan-test; The resulting data frame `input_cell_selection()` should return the currently selected cells.

# Load the dataset
penguins = load_penguins_raw()
df = penguins
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import pandas as pd
import seaborn as sns

from shiny import App, Inputs, Outputs, Session, reactive, render, ui

df = pd.DataFrame(
sns.load_dataset( # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
"iris"
) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
)

distinct_df = df.drop_duplicates(subset=["species"])
app_ui = ui.page_fluid(
ui.row(
ui.column(
6,
ui.h2("Iris Dataset"),
),
ui.column(2, ui.input_action_button("reset_df", "Reset Dataframe")),
),
ui.output_data_frame("iris_df"),
ui.h2("Data view indices"),
ui.output_text_verbatim("data_view_indices"),
ui.h2("Indices when view_selected=True"),
ui.output_text_verbatim("data_view_selected_true"),
ui.h2("Indices when view_selected=False"),
ui.output_text_verbatim("data_view_selected_false"),
ui.h2("Show selected cell"),
ui.output_text_verbatim("cell_selection"),
)


def server(input: Inputs, output: Outputs, session: Session) -> None:

@render.data_frame
def iris_df():
return render.DataGrid(
data=distinct_df, # pyright: ignore[reportUnknownArgumentType]
filters=True,
selection_mode="rows",
)

@render.code # pyright: ignore[reportArgumentType]
def data_view_indices():
return iris_df._input_data_view_indices()

@render.code # pyright: ignore[reportArgumentType]
def data_view_selected_false(): # pyright: ignore[reportUnknownParameterType]
return iris_df.data_view(
selected=False
).index.values # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]

@render.code # pyright: ignore[reportArgumentType]
def data_view_selected_true(): # pyright: ignore[reportUnknownParameterType]
return iris_df.data_view(
selected=True
).index.values # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]

@render.code # pyright: ignore[reportArgumentType]
def cell_selection(): # pyright: ignore[reportUnknownParameterType]
return iris_df.input_cell_selection()["rows"] # pyright: ignore

@reactive.Effect
@reactive.event(input.reset_df)
def reset_df():
iris_df._reset_reactives()


app = App(app_ui, server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from conftest import ShinyAppProc
from controls import InputActionButton, OutputCode, OutputDataFrame
from playwright.sync_api import Page


def test_dataframe_organization_methods(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)
data_frame = OutputDataFrame(page, "iris_df")
input_view_indices = OutputCode(page, "data_view_indices")
input_view_selected_true = OutputCode(page, "data_view_selected_true")
input_view_selected_false = OutputCode(page, "data_view_selected_false")
input_cell_selection = OutputCode(page, "cell_selection")
reset_df = InputActionButton(page, "reset_df")

# assert value of unsorted table
input_view_indices.expect_value("(0, 1, 2)")
input_view_selected_true.expect_value("[]")
input_view_selected_false.expect_value("[ 0 50 100]")
input_cell_selection.expect_value("()")

# sort column by number descending
data_frame.set_column_sort(col=0)
input_view_indices.expect_value("(1, 2, 0)")
input_view_selected_true.expect_value("[]")
input_view_selected_false.expect_value("[ 50 100 0]")
input_cell_selection.expect_value("()")

# sort column by number ascending
data_frame.set_column_sort(col=0)
input_view_indices.expect_value("(0, 2, 1)")
input_view_selected_true.expect_value("[]")
input_view_selected_false.expect_value("[ 0 100 50]")
input_cell_selection.expect_value("()")

# sort column by text ascending
data_frame.set_column_sort(col=4)
input_view_indices.expect_value("(0, 1, 2)")
input_view_selected_true.expect_value("[]")
input_view_selected_false.expect_value("[ 0 50 100]")
input_cell_selection.expect_value("()")

# sort column by text descending
data_frame.set_column_sort(col=4)
input_view_indices.expect_value("(2, 1, 0)")
input_view_selected_true.expect_value("[]")
input_view_selected_false.expect_value("[100 50 0]")
input_cell_selection.expect_value("()")

# reset dataframe
reset_df.click()

# filter using numbers
data_frame.set_column_filter(col=0, text=["6", "7"])
input_view_indices.expect_value("(1, 2)")
input_view_selected_true.expect_value("[]")
input_view_selected_false.expect_value("[ 50 100]")
input_cell_selection.expect_value("()")

# reset dataframe
reset_df.click()

# select multiple rows
data_frame.select_rows([0, 2])
input_view_indices.expect_value("(0, 1, 2)")
input_view_selected_true.expect_value("[ 0 100]")
input_view_selected_false.expect_value("[ 0 50 100]")
input_cell_selection.expect_value("(0, 2)")

# reset dataframe
reset_df.click()

# select single row
data_frame.select_rows([0])
input_view_indices.expect_value("(0, 1, 2)")
input_view_selected_true.expect_value("[0]")
input_view_selected_false.expect_value("[ 0 50 100]")
input_cell_selection.expect_value("(0,)")
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pandas as pd
import seaborn as sns

from shiny import App, Inputs, Outputs, Session, render, ui

df = pd.DataFrame(
sns.load_dataset(
"iris"
) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
)
app_ui = ui.page_fluid(
ui.h2("Iris Dataset"),
ui.output_data_frame("iris_df"),
)

df["sepal_length"] = df["sepal_length"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
df["sepal_width"] = df["sepal_width"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
df["petal_width"] = df["petal_width"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore
df["species"] = df["species"].apply(lambda x: ui.HTML(f"<u>{x}</u>")) # type: ignore


def server(input: Inputs, output: Outputs, session: Session) -> None:

@render.data_frame
def iris_df():
return render.DataGrid(
data=df.head(), # pyright: ignore[reportUnknownArgumentType]
editable=True,
)


app = App(app_ui, server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from conftest import ShinyAppProc
from controls import OutputDataFrame
from playwright.sync_api import Page


def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

data_frame = OutputDataFrame(page, "iris_df")
# 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)
data_frame.expect_cell("1.4", row=0, col=2)
data_frame.save_cell("152", row=0, col=2, save_key="Shift+Tab")
data_frame.expect_cell("1.4", row=0, col=2)
data_frame.expect_cell_class("cell-edit-editing", row=0, col=2)

# 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)
data_frame.expect_cell("1.4", row=0, col=2)
data_frame.save_cell("152", row=0, col=2, save_key="Tab")
data_frame.expect_cell("1.4", row=0, col=2)
data_frame.expect_cell_class("cell-edit-editing", row=0, col=2)
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,21 @@ def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None:
# Filter using a range for a column that contains numbers
data_frame.set_column_filter(1, text=["40", "50"])
data_frame.expect_cell("40", row=0, col=1)

# Editing a cell in the first row and hitting `shift+enter` should not submit the change and stay editing the current cell
data_frame.expect_cell("N25A2", row=0, col=6)
data_frame.save_cell("NAAAAA", row=0, col=6, save_key="Shift+Enter")
data_frame.expect_cell("N25A2", row=0, col=6)
data_frame.save_cell("NAAAAA", row=0, col=6, save_key="Escape")
data_frame.expect_cell("N25A2", row=0, col=6)

# Editing a cell in the last row and hitting `enter` should not submit the change and stay editing the current cell
# data_frame.set_column_filter(7, text="No")
# Test scrolling to last row
data_frame.save_cell("NAAAAA", row=7, col=6, save_key="Enter")
data_frame.expect_cell("N29A2", row=7, col=6)
data_frame.save_cell("NAAAAA", row=7, col=6, save_key="Escape")
data_frame.expect_cell("N29A2", row=7, col=6)

# Test scrolling up to top
data_frame.expect_cell("N25A2", row=0, col=6)

0 comments on commit 7cd9e5e

Please sign in to comment.