Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import datetime
import functools
import logging
import subprocess
import sys
Expand All @@ -24,6 +25,7 @@
"local_app",
"run_shiny_app",
"expect_to_change",
"retry_with_timeout",
)

here = PurePath(__file__).parent
Expand Down Expand Up @@ -238,11 +240,57 @@ def expect_to_change(
page.keyboard.send_keys("hello")

"""

original_value = func()
yield
start = time.time()
while time.time() - start < timeoutSecs:
time.sleep(0.1)
if func() != original_value:
return
raise TimeoutError("Timeout while waiting for change")

@retry_with_timeout(timeoutSecs)
def wait_for_change():
if func() == original_value:
raise AssertionError("Value did not change")

wait_for_change()


def retry_with_timeout(timeout: float = 30):
"""
Decorator that retries a function until 1) it succeeds, 2) fails with a
non-assertion error, or 3) repeatedly fails with an AssertionError for longer than
the timeout. If the timeout elapses, the last AssertionError is raised.

Parameters
----------
timeout
How long to wait for the function to succeed before raising the last
AssertionError.

Returns
-------
A decorator that can be applied to a function.

Example
-------

@retry_with_timeout(30)
def try_to_find_element():
if not page.locator("#name").exists():
raise AssertionError("Element not found")

try_to_find_element()
"""

def decorator(func: Callable[[], None]) -> Callable[[], None]:
@functools.wraps(func)
def wrapper() -> None:
start = time.time()
while True:
try:
return func()
except AssertionError as e:
if time.time() - start > timeout:
raise e
time.sleep(0.1)

return wrapper

return decorator
162 changes: 136 additions & 26 deletions e2e/data_frame/test_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@


import re
import time
from typing import Any, Callable

import pytest
from conftest import ShinyAppProc, create_example_fixture, expect_to_change
from controls import InputSelectize, InputSwitch
from playwright.sync_api import Locator, Page, expect

RERUNS = 3

data_frame_app = create_example_fixture("dataframe")


Expand Down Expand Up @@ -37,7 +38,7 @@ def do():
return do


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_grid_mode(
page: Page, data_frame_app: ShinyAppProc, grid: Locator, grid_container: Locator
):
Expand All @@ -51,23 +52,23 @@ def test_grid_mode(
expect(grid_container).to_have_class(re.compile(r"\bshiny-data-grid-grid\b"))


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_summary_navigation(
page: Page, data_frame_app: ShinyAppProc, grid_container: Locator, summary: Locator
):
page.goto(data_frame_app.url)

# Check that summary responds to navigation
expect(summary).to_have_text("Viewing rows 1 through 10 of 20")
expect(summary).to_have_text(re.compile("^Viewing rows 1 through \\d+ of 20$"))
# Put focus in the table and hit End keystroke
grid_container.locator("tbody tr:first-child td:first-child").click()
with expect_to_change(lambda: summary.inner_text()):
page.keyboard.press("End")
# Ensure that summary updated
expect(summary).to_have_text("Viewing rows 11 through 20 of 20")
expect(summary).to_have_text(re.compile("^Viewing rows \\d+ through 20 of 20$"))


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_full_width(page: Page, data_frame_app: ShinyAppProc, grid_container: Locator):
page.goto(data_frame_app.url)

Expand All @@ -87,7 +88,7 @@ def get_width() -> float:
InputSwitch(page, "fullwidth").toggle()


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_table_switch(
page: Page,
data_frame_app: ShinyAppProc,
Expand All @@ -107,17 +108,18 @@ def test_table_switch(
expect(grid_container).to_have_class(re.compile(r"\bshiny-data-grid-table\b"))

# Switching modes resets scroll
expect(summary).to_have_text("Viewing rows 1 through 10 of 20")
expect(summary).to_have_text(re.compile("^Viewing rows 1 through \\d+ of 20$"))

scroll_to_end()
expect(summary).to_have_text("Viewing rows 11 through 20 of 20")
expect(summary).to_have_text(re.compile("^Viewing rows \\d+ through 20 of 20$"))

# Switch datasets to much longer one
select_dataset.set("diamonds")
expect(summary).to_have_text("Viewing rows 1 through 10 of 53940")
select_dataset.expect.to_have_value("diamonds")
expect(summary).to_have_text(re.compile("^Viewing rows 1 through \\d+ of 53940$"))


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_sort(
page: Page,
data_frame_app: ShinyAppProc,
Expand All @@ -126,6 +128,7 @@ def test_sort(
page.goto(data_frame_app.url)
select_dataset = InputSelectize(page, "dataset")
select_dataset.set("diamonds")
select_dataset.expect.to_have_value("diamonds")

# Test sorting
header_clarity = grid_container.locator("tr:first-child th:nth-child(4)")
Expand All @@ -143,7 +146,7 @@ def test_sort(
expect(first_cell_depth).to_have_text("67.6")


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_multi_selection(
page: Page, data_frame_app: ShinyAppProc, grid_container: Locator, snapshot: Any
):
Expand Down Expand Up @@ -173,7 +176,7 @@ def detail_text():
assert detail_text() == snapshot


@pytest.mark.flaky
@pytest.mark.flaky(reruns=RERUNS)
def test_single_selection(
page: Page, data_frame_app: ShinyAppProc, grid_container: Locator, snapshot: Any
):
Expand Down Expand Up @@ -204,18 +207,125 @@ def detail_text():
assert detail_text() == snapshot


def retry_with_timeout(timeout: float = 30):
def decorator(func: Callable[[], None]) -> None:
def exec() -> None:
start = time.time()
while True:
try:
return func()
except AssertionError as e:
if time.time() - start > timeout:
raise e
time.sleep(0.1)
def test_filter_grid(
page: Page,
data_frame_app: ShinyAppProc,
grid: Locator,
summary: Locator,
snapshot: Any,
):
page.goto(data_frame_app.url)
_filter_test_impl(page, data_frame_app, grid, summary, snapshot)


def test_filter_table(
page: Page,
data_frame_app: ShinyAppProc,
grid: Locator,
grid_container: Locator,
summary: Locator,
snapshot: Any,
):
page.goto(data_frame_app.url)

exec()
InputSwitch(page, "gridstyle").toggle()
expect(grid_container).not_to_have_class(re.compile(r"\bshiny-data-grid-grid\b"))
expect(grid_container).to_have_class(re.compile(r"\bshiny-data-grid-table\b"))

_filter_test_impl(page, data_frame_app, grid, summary, snapshot)


def _filter_test_impl(
page: Page,
data_frame_app: ShinyAppProc,
grid: Locator,
summary: Locator,
snapshot: Any,
):
filters = grid.locator("tr.filters")

filter_subidir_min = filters.locator("> th:nth-child(1) > div > input:nth-child(1)")
filter_subidir_max = filters.locator("> th:nth-child(1) > div > input:nth-child(2)")
filter_attnr = filters.locator("> th:nth-child(2) > input")
filter_num1_min = filters.locator("> th:nth-child(3) > div > input:nth-child(1)")
filter_num1_max = filters.locator("> th:nth-child(3) > div > input:nth-child(2)")

expect(filter_subidir_min).to_be_visible()
expect(filter_subidir_max).to_be_visible()
expect(filter_attnr).to_be_visible()
expect(filter_num1_min).to_be_visible()
expect(filter_num1_max).to_be_visible()

expect(summary).to_be_visible()
expect(summary).to_have_text(re.compile(" of 20$"))

# Placeholder text only appears when filter is focused
expect(page.get_by_placeholder("Min (1)", exact=True)).not_to_be_attached()
expect(page.get_by_placeholder("Max (20)", exact=True)).not_to_be_attached()
filter_subidir_min.focus()
expect(page.get_by_placeholder("Min (1)", exact=True)).to_be_attached()
expect(page.get_by_placeholder("Max (20)", exact=True)).to_be_attached()
filter_subidir_min.blur()
expect(page.get_by_placeholder("Min (1)", exact=True)).not_to_be_attached()
expect(page.get_by_placeholder("Max (20)", exact=True)).not_to_be_attached()

# Make sure that filtering input results in correct number of rows

# Test only min
filter_subidir_min.fill("5")
expect(summary).to_have_text(re.compile(" of 16$"))
# Test min and max
filter_subidir_max.fill("14")
expect(summary).to_have_text(re.compile(" of 10$"))

# When filtering results in all rows being shown, the summary should not be visible
filter_subidir_max.fill("11")
expect(summary).not_to_be_attached()

# Test only max
filter_subidir_min.fill("")
expect(summary).to_have_text(re.compile(" of 11"))

filter_subidir_min.clear()
filter_subidir_max.clear()

# Try substring search
filter_attnr.fill("oc")
expect(summary).to_have_text(re.compile(" of 10"))
filter_num1_min.focus()
# Ensure other columns' filter placeholders show faceted results
expect(page.get_by_placeholder("Min (5)", exact=True)).to_be_attached()
expect(page.get_by_placeholder("Max (8)", exact=True)).to_be_attached()

# Filter down to zero matching rows
filter_attnr.fill("q")
# Summary should be gone
expect(summary).not_to_be_attached()
filter_num1_min.focus()
# Placeholders should not have values
expect(page.get_by_placeholder("Min", exact=True)).to_be_attached()
expect(page.get_by_placeholder("Max", exact=True)).to_be_attached()

filter_attnr.clear()

# Apply multiple filters, make sure we get the correct results
filter_subidir_max.fill("8")
filter_num1_min.fill("4")
expect(grid.locator("tbody tr")).to_have_count(5)

# Ensure changing dataset resets filters
select_dataset = InputSelectize(page, "dataset")
select_dataset.set("attention")
select_dataset.expect.to_have_value("attention")
expect(page.get_by_text("Unnamed: 0")).to_be_attached()
select_dataset.set("anagrams")
select_dataset.expect.to_have_value("anagrams")
expect(summary).to_have_text(re.compile(" of 20"))


def test_filter_disable(page: Page, data_frame_app: ShinyAppProc):
page.goto(data_frame_app.url)

return decorator
expect(page.locator("tr.filters")).to_be_attached()
InputSwitch(page, "filters").toggle()
expect(page.locator("tr.filters")).not_to_be_attached()
4 changes: 2 additions & 2 deletions e2e/inputs/test_input_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ def test_input_slider_kitchen(page: Page, slider_app: ShinyAppProc) -> None:

# # Duplicate logic of next test. Only difference is `max_err_values=15`
# try:
# obs.set("not-a-number", timeout=200)
# obs.set("not-a-number", timeout=800)
# except ValueError as e:
# values_found = '"10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", ...'
# assert values_found in str(
# e
# ), "Error message should contain the list of first 15 valid values"

try:
obs.set("not-a-number", timeout=200, max_err_values=4)
obs.set("not-a-number", timeout=800, max_err_values=4)
except ValueError as e:
values_found = '"10", "11", "12", "13", ...'
assert values_found in str(
Expand Down
11 changes: 7 additions & 4 deletions examples/dataframe/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def app_ui(req):
{"none": "(None)", "single": "Single", "multiple": "Multiple"},
selected="multiple",
),
ui.input_switch("filters", "Filters", True),
ui.input_switch("gridstyle", "Grid", True),
ui.input_switch("fullwidth", "Take full width", True),
ui.output_data_frame("grid"),
Expand Down Expand Up @@ -68,16 +69,18 @@ def grid():
if input.gridstyle():
return render.DataGrid(
df(),
row_selection_mode=input.selection_mode(),
height=height,
width=width,
height=height,
filters=input.filters(),
row_selection_mode=input.selection_mode(),
)
else:
return render.DataTable(
df(),
row_selection_mode=input.selection_mode(),
height=height,
width=width,
height=height,
filters=input.filters(),
row_selection_mode=input.selection_mode(),
)

@reactive.Effect
Expand Down
Loading