diff --git a/setup.cfg b/setup.cfg index 2e4b31848..d63ae762a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ test = pytest>=6.2.4 pytest-asyncio>=0.17.2 pytest-playwright>=0.3.0 + playwright>=1.43.0 pytest-xdist pytest-timeout pytest-rerunfailures diff --git a/tests/playwright/shiny/components/busy_indicators/app.py b/tests/playwright/shiny/components/busy_indicators/app.py new file mode 100644 index 000000000..3c65b8b87 --- /dev/null +++ b/tests/playwright/shiny/components/busy_indicators/app.py @@ -0,0 +1,80 @@ +# pyright:basic +import time + +import numpy as np +import seaborn as sns + +from shiny import App, module, reactive, render, ui + + +# -- Reusable card module -- +@module.ui +def card_ui(spinner_type, spinner_color, spinner_size): + return ui.card( + ui.busy_indicators.options( + spinner_type=spinner_type, + spinner_color=spinner_color, + spinner_size=spinner_size, + ), + ui.card_header("Spinner: " + spinner_type), + ui.output_plot("plot"), + ) + + +@module.server +def card_server(input, output, session, rerender): + @render.plot + def plot(): + rerender() + time.sleep(0.5) + sns.lineplot(x=np.arange(100), y=np.random.randn(100)) + + +# -- Main app -- +app_ui = ui.page_fillable( + ui.busy_indicators.options( + pulse_background="linear-gradient(45deg, blue, red)", + pulse_height="100px", + pulse_speed="4s", + ), + # ui.busy_indicators.use(spinners=False, pulse=True), + ui.input_radio_buttons( + "busy_indicator_type", + "Choose the indicator type", + ["spinners", "pulse"], + inline=True, + ), + ui.input_task_button("rerender", "Re-render"), + ui.layout_columns( + card_ui("ring", "ring", "red", "10px"), + card_ui("bars", "bars", "green", "20px"), + card_ui("dots", "dots", "blue", "30px"), + card_ui("pulse", "pulse", "olive", "50px"), + col_widths=6, + ), + ui.output_ui("indicator_types_ui"), +) + + +def server(input, output, session): + + @reactive.calc + @reactive.event(input.rerender, ignore_none=False) + def rerender(): + return input.rerender() + + card_server("ring", rerender=rerender) + card_server("bars", rerender=rerender) + card_server("dots", rerender=rerender) + card_server("pulse", rerender=rerender) + + @render.ui + def indicator_types_ui(): + selected_busy_indicator_type = input.busy_indicator_type() + return ui.busy_indicators.use( + spinners=(selected_busy_indicator_type == "spinners"), + pulse=(selected_busy_indicator_type != "spinners"), + ) + + +app = App(app_ui, server, debug=True) diff --git a/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py b/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py new file mode 100644 index 000000000..7ecb487f9 --- /dev/null +++ b/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py @@ -0,0 +1,56 @@ +import os +from urllib.parse import urlparse + +from conftest import ShinyAppProc +from controls import InputRadioButtons, InputTaskButton, expect_not_to_have_class +from playwright.sync_api import Page, expect + + +def get_spinner_computed_property( + page: Page, element_id: str, property_name: str +) -> str: + expect(page.locator(element_id)).to_be_visible() + return page.evaluate( + f"window.getComputedStyle(document.querySelector('{element_id}'), '::after').getPropertyValue('{property_name}');" + ) + + +def get_pulse_computed_property(page: Page, property_name: str) -> str: + expect(page.locator("html body")).to_be_visible() + return page.evaluate( + f"window.getComputedStyle(document.documentElement, '::after').getPropertyValue('{property_name}');" + ) + + +def test_busy_indicators(page: Page, local_app: ShinyAppProc) -> None: + page.goto(local_app.url) + spinner_type = InputRadioButtons(page, "busy_indicator_type") + render_button = InputTaskButton(page, "rerender") + + # Verify spinner indicator behavior + spinner_properties = [ + ("#pulse-plot", "50px", "rgb(128, 128, 0)", "pulse.svg"), + ("#ring-plot", "10px", "rgb(255, 0, 0)", "ring.svg"), + ("#bars-plot", "20px", "rgb(0, 128, 0)", "bars.svg"), + ("#dots-plot", "30px", "rgb(0, 0, 255)", "dots.svg"), + ] + + for element_id, height, background_color, svg_name in spinner_properties: + assert get_spinner_computed_property(page, element_id, "height") == height + assert ( + get_spinner_computed_property(page, element_id, "background-color") + == background_color + ) + mask_image_url = get_spinner_computed_property(page, element_id, "mask-image") + assert os.path.basename(urlparse(mask_image_url).path).rstrip('")') == svg_name + assert get_spinner_computed_property(page, element_id, "width") == height + + # Verify pulse indicator behavior + expect_not_to_have_class(page.locator("html"), "shiny-busy", timeout=8000) + spinner_type.set("pulse") + render_button.click() + assert get_pulse_computed_property(page, "height") == "100px" + assert ( + get_pulse_computed_property(page, "background-image") + == "linear-gradient(45deg, rgb(0, 0, 255), rgb(255, 0, 0))" + )