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
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* Expose `shiny.playwright`, `shiny.run`, and `shiny.pytest` modules that allow users to testing their Shiny apps. (#1448, #1456, #1481)
* `shiny.playwright` contains `controller` and `expect` submodules. `controller` will contain many classes to interact with (and verify!) your Shiny app using Playwright. `expect` contains expectation functions that enhance standard Playwright expectation methods.
* `shiny.run` contains the `run_shiny_app` command and the return type `ShinyAppProc`. `ShinyAppProc` can be used to type the Shiny app pytest fixtures.
* `shiny.pytest` contains pytest test fixtures. The `local_app` pytest fixture is automatically available and runs a sibling `app.py` file. Where as `create_app_fixture(PATH_TO_APP)` allows for a Shiny app to be instantiated from a different folder.

* Added CLI command `shiny add test` to add a test file to an existing Shiny app. (#1461)

* `@render.data_frame` has added a few new methods:
* `.data_view_rows()` is a reactive value representing the sorted and filtered row numbers. This value wraps `input.<ID>_data_view_rows()`(#1374)
* `.sort()` is a reactive value representing the sorted column information (dictionaries containing `col: int` and `desc: bool`). This value wraps `input.<ID>_sort()`. (#1374)
Expand Down Expand Up @@ -60,8 +67,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* Expose shiny.pytest, shiny.run and shiny.playwright modules that allow users to testing their Shiny apps. (#1448, #1456)

* Added busy indicators to provide users with a visual cue when the server is busy calculating outputs or otherwise serving requests to the client. More specifically, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. Use the new `ui.busy_indicator.options()` function to customize the appearance of the busy indicators and `ui.busy_indicator.use()` to disable/enable them. (#918)

* Added support for creating modules using Shiny Express syntax, and using modules in Shiny Express apps. (#1220)
Expand Down
100 changes: 50 additions & 50 deletions docs/_quartodoc-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,68 +11,68 @@ quartodoc:
show_signature_annotations: false
sections:
- title: UI Layouts
desc: Methods for interacting with Shiny app multiple UI component controls.
desc: Methods for interacting with Shiny app multiple UI component controller.
contents:
- playwright.controls.Accordion
- playwright.controls.AccordionPanel
- playwright.controls.Card
- playwright.controls.Popover
- playwright.controls.Sidebar
- playwright.controls.Tooltip
- playwright.controller.Accordion
- playwright.controller.AccordionPanel
- playwright.controller.Card
- playwright.controller.Popover
- playwright.controller.Sidebar
- playwright.controller.Tooltip
- title: UI Inputs
desc: Methods for interacting with Shiny app input value controls.
desc: Methods for interacting with Shiny app input value controller.
contents:
- playwright.controls.InputActionLink
- playwright.controls.InputCheckbox
- playwright.controls.InputCheckboxGroup
- playwright.controls.InputDarkMode
- playwright.controls.InputDate
- playwright.controls.InputDateRange
- playwright.controls.InputFile
- playwright.controls.InputNumeric
- playwright.controls.InputPassword
- playwright.controls.InputRadioButtons
- playwright.controls.InputSelect
- playwright.controls.InputSelectize
- playwright.controls.InputSlider
- playwright.controls.InputSliderRange
- playwright.controls.InputSwitch
- playwright.controls.InputTaskButton
- playwright.controls.InputText
- playwright.controls.InputTextArea
- playwright.controller.InputActionLink
- playwright.controller.InputCheckbox
- playwright.controller.InputCheckboxGroup
- playwright.controller.InputDarkMode
- playwright.controller.InputDate
- playwright.controller.InputDateRange
- playwright.controller.InputFile
- playwright.controller.InputNumeric
- playwright.controller.InputPassword
- playwright.controller.InputRadioButtons
- playwright.controller.InputSelect
- playwright.controller.InputSelectize
- playwright.controller.InputSlider
- playwright.controller.InputSliderRange
- playwright.controller.InputSwitch
- playwright.controller.InputTaskButton
- playwright.controller.InputText
- playwright.controller.InputTextArea
- title: Value boxes
desc: Methods for interacting with Shiny app valuebox controls.
desc: Methods for interacting with Shiny app valuebox controller.
contents:
- playwright.controls.ValueBox
- playwright.controller.ValueBox
- title: Navigation (tab) panels
desc: Methods for interacting with Shiny app UI content controls.
desc: Methods for interacting with Shiny app UI content controller.
contents:
- playwright.controls.NavItem
- playwright.controls.NavsetBar
- playwright.controls.NavsetCardPill
- playwright.controls.NavsetCardTab
- playwright.controls.NavsetCardUnderline
- playwright.controls.NavsetHidden
- playwright.controls.NavsetPill
- playwright.controls.NavsetPillList
- playwright.controls.NavsetTab
- playwright.controls.NavsetUnderline
- playwright.controller.NavItem
- playwright.controller.NavsetBar
- playwright.controller.NavsetCardPill
- playwright.controller.NavsetCardTab
- playwright.controller.NavsetCardUnderline
- playwright.controller.NavsetHidden
- playwright.controller.NavsetPill
- playwright.controller.NavsetPillList
- playwright.controller.NavsetTab
- playwright.controller.NavsetUnderline
- title: Upload and download
desc: Methods for interacting with Shiny app uploading and downloading controls.
desc: Methods for interacting with Shiny app uploading and downloading controller.
contents:
- playwright.controls.DownloadButton
- playwright.controls.DownloadLink
- playwright.controller.DownloadButton
- playwright.controller.DownloadLink
- title: Rendering outputs
desc: Render output in a variety of ways.
contents:
- playwright.controls.OutputCode
- playwright.controls.OutputDataFrame
- playwright.controls.OutputImage
- playwright.controls.OutputPlot
- playwright.controls.OutputTable
- playwright.controls.OutputText
- playwright.controls.OutputTextVerbatim
- playwright.controls.OutputUi
- playwright.controller.OutputCode
- playwright.controller.OutputDataFrame
- playwright.controller.OutputImage
- playwright.controller.OutputPlot
- playwright.controller.OutputTable
- playwright.controller.OutputText
- playwright.controller.OutputTextVerbatim
- playwright.controller.OutputUi
- title: "Playwright Expect"
desc: "Methods for testing the state of a locator within a Shiny app."
contents:
Expand Down
8 changes: 4 additions & 4 deletions shiny/_template_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,20 +399,20 @@ def path_does_not_exist(x: Path) -> bool | str:
f"""\
from playwright.sync_api import Page

from shiny.playwright.controls import <IMPORT REQUIRED CONTROLS>
from shiny.playwright import controller
from shiny.run import ShinyAppProc


def {test_name}(page: Page, local_app: ShinyAppProc):

page.goto(local_app.url)
# Add tests code here
# Add test code here
"""
if is_same_dir
else f"""\
from playwright.sync_api import Page

from shiny.playwright.controls import <IMPORT REQUIRED CONTROLS>
from shiny.playwright import controller
from shiny.pytest import create_app_fixture
from shiny.run import ShinyAppProc

Expand All @@ -422,7 +422,7 @@ def {test_name}(page: Page, local_app: ShinyAppProc):
def {test_name}(page: Page, app: ShinyAppProc):

page.goto(app.url)
# Add tests code here
# Add test code here
"""
)
# Make sure test file directory exists
Expand Down
4 changes: 2 additions & 2 deletions shiny/playwright/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
)
except ImportError:
pass
from . import controls, expect
from . import controller, expect

__all__ = ["expect", "controls"]
__all__ = ["expect", "controller"]
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
skip_if_not_chrome,
)

from shiny.playwright.controls import Accordion
from shiny.playwright import controller

app_url = create_deploys_app_url_fixture("shiny_express_accordion")

Expand All @@ -17,7 +17,7 @@
def test_express_accordion(page: Page, app_url: str) -> None:
page.goto(app_url)

acc = Accordion(page, "express_accordion")
acc = controller.Accordion(page, "express_accordion")
acc_panel_2 = acc.accordion_panel("Panel 2")
acc_panel_2.expect_open(True)
acc_panel_2.expect_body("n = 50")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
skip_if_not_chrome,
)

from shiny.playwright.controls import OutputDataFrame
from shiny.playwright import controller

app_url = create_deploys_app_url_fixture("shiny-express-dataframe")

Expand All @@ -17,5 +17,5 @@
def test_express_dataframe_deploys(page: Page, app_url: str) -> None:
page.goto(app_url)

dataframe = OutputDataFrame(page, "sample_data_frame")
dataframe = controller.OutputDataFrame(page, "sample_data_frame")
dataframe.expect_n_row(6)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
skip_if_not_chrome,
)

from shiny.playwright.controls import NavsetTab
from shiny.playwright import controller

TIMEOUT = 2 * 60 * 1000

Expand All @@ -26,12 +26,12 @@ def test_page_default(page: Page, app_url: str) -> None:

# Perform these tests second as their locators are not stable over time.
# (They require that a locator be realized before finding the second locator)
nav_html = NavsetTab(page, "express_navset_tab")
nav_html = controller.NavsetTab(page, "express_navset_tab")
nav_html.expect_content("pre 0pre 1pre 2")
nav_html.set("div")
nav_html.expect_content("div 0\ndiv 1\ndiv 2")
nav_html.set("span")
nav_html.expect_content("span 0span 1span 2")

navset_card_tab = NavsetTab(page, "express_navset_card_tab")
navset_card_tab = controller.NavsetTab(page, "express_navset_card_tab")
navset_card_tab.expect_content("")
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
skip_if_not_chrome,
)

from shiny.playwright.controls import Card, OutputTextVerbatim
from shiny.playwright import controller

app_url = create_deploys_app_url_fixture("express_page_fillable")

Expand All @@ -17,8 +17,8 @@
def test_express_page_fillable(page: Page, app_url: str) -> None:
page.goto(app_url)

card = Card(page, "card")
output_txt = OutputTextVerbatim(page, "txt")
card = controller.Card(page, "card")
output_txt = controller.OutputTextVerbatim(page, "txt")
output_txt.expect_value("50")
bounding_box = card.loc.bounding_box()
assert bounding_box is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
skip_if_not_chrome,
)

from shiny.playwright.controls import Card, OutputTextVerbatim
from shiny.playwright import controller

app_url = create_deploys_app_url_fixture("express_page_fluid")

Expand All @@ -17,8 +17,8 @@
def test_express_page_fluid(page: Page, app_url: str) -> None:
page.goto(app_url)

card = Card(page, "card")
output_txt = OutputTextVerbatim(page, "txt")
card = controller.Card(page, "card")
output_txt = controller.OutputTextVerbatim(page, "txt")
output_txt.expect_value("50")
bounding_box = card.loc.bounding_box()
assert bounding_box is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
skip_if_not_chrome,
)

from shiny.playwright.controls import OutputTextVerbatim, Sidebar
from shiny.playwright import controller

app_url = create_deploys_app_url_fixture("express_page_sidebar")

Expand All @@ -17,7 +17,7 @@
def test_express_page_sidebar(page: Page, app_url: str) -> None:
page.goto(app_url)

sidebar = Sidebar(page, "sidebar")
sidebar = controller.Sidebar(page, "sidebar")
sidebar.expect_text("SidebarTitle Sidebar Content")
output_txt = OutputTextVerbatim(page, "txt")
output_txt = controller.OutputTextVerbatim(page, "txt")
output_txt.expect_value("50")
12 changes: 4 additions & 8 deletions tests/playwright/shiny/async/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,22 @@

from playwright.sync_api import Page, expect

from shiny.playwright.controls import (
InputActionButton,
InputTextArea,
OutputTextVerbatim,
)
from shiny.playwright import controller
from shiny.run import ShinyAppProc


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

InputTextArea(page, "value").set("Hello\nGoodbye")
InputActionButton(page, "go").click()
controller.InputTextArea(page, "value").set("Hello\nGoodbye")
controller.InputActionButton(page, "go").click()

# TODO-future; Make into proper class
progress = page.locator("#shiny-notification-panel")
expect(progress).to_be_visible()
expect(progress).to_contain_text("Calculating...")

OutputTextVerbatim(page, "hash_output").expect_value(
controller.OutputTextVerbatim(page, "hash_output").expect_value(
"2e220fb9d401bf832115305b9ae0277e7b8b1a9368c6526e450acd255e0ec0c2", timeout=2000
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from playwright.sync_api import Page, expect

from shiny.playwright.controls import InputActionButton, InputSlider, OutputTextVerbatim
from shiny.playwright import controller
from shiny.run import ShinyAppProc


Expand All @@ -16,9 +16,9 @@ def check_case(
min: tuple[Optional[str], Optional[str]] = (None, None),
max: tuple[Optional[str], Optional[str]] = (None, None),
):
slider_times = InputSlider(page, f"{id}-times")
btn_reset = InputActionButton(page, f"{id}-reset")
out_txt = OutputTextVerbatim(page, f"{id}-txt")
slider_times = controller.InputSlider(page, f"{id}-times")
btn_reset = controller.InputActionButton(page, f"{id}-reset")
out_txt = controller.OutputTextVerbatim(page, f"{id}-txt")

if value[0] is not None:
out_txt.expect_value(value[0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from colors import bg_color, fg_color
from playwright.sync_api import Page, expect

from shiny.playwright.controls import Sidebar
from shiny.playwright.controls._controls import _expect_class_value
from shiny.playwright import controller
from shiny.playwright.controller._controls import _expect_class_value
from shiny.run import ShinyAppProc


Expand Down Expand Up @@ -38,7 +38,7 @@ def test_sidebar_bg_colors(page: Page, local_app: ShinyAppProc) -> None:
expect(sidebar).to_have_css("background-color", bg_color)
expect(sidebar).to_have_css("color", fg_color)

s1 = Sidebar(page, "s1")
s1 = controller.Sidebar(page, "s1")
s1.expect_position("left")
s2 = Sidebar(page, "s2")
s2 = controller.Sidebar(page, "s2")
s2.expect_position("right")
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from playwright.sync_api import Page

from shiny.playwright.controls import OutputCode, OutputDataFrame
from shiny.playwright import controller
from shiny.run import ShinyAppProc


Expand All @@ -12,9 +12,9 @@ def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
# The purpose of this test is to make sure that the data grid can work on Pandas
# data frames that use an index that is not simply 0-based integers.

grid = OutputDataFrame(page, "grid")
detail = OutputDataFrame(page, "detail")
selected_rows = OutputCode(page, "selected_rows")
grid = controller.OutputDataFrame(page, "grid")
detail = controller.OutputDataFrame(page, "detail")
selected_rows = controller.OutputCode(page, "selected_rows")

grid.expect_cell("three", row=2, col=0)
detail.expect_n_row(0)
Expand Down
Loading