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
3 changes: 1 addition & 2 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ _sidebar.yml
/.quarto/
objects.json
site_libs/
_objects_core.json
_objects_express.json
_objects_*.json
10 changes: 9 additions & 1 deletion docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ deps: $(PYBIN) dev-htmltools dev-shinylive ## Install build dependencies
$(PYBIN)/pip install pip --upgrade
$(PYBIN)/pip install ..[doc]

quartodoc: quartodoc_build_core quartodoc_build_express quartodoc_post ## Build quartodocs for express and core
quartodoc: quartodoc_build_core quartodoc_build_express quartodoc_build_test quartodoc_post ## Build quartodocs for express and core

## Build interlinks for API docs
quartodoc_interlinks: $(PYBIN)
Expand Down Expand Up @@ -78,6 +78,14 @@ quartodoc_build_express: $(PYBIN) quartodoc_interlinks
&& mv objects.json _objects_express.json \
&& echo "::endgroup::"

## Build test API docs
quartodoc_build_test: $(PYBIN) quartodoc_interlinks
. $(PYBIN)/activate \
&& echo "::group::quartodoc build testing docs" \
&& quartodoc build --config _quartodoc-testing.yml --verbose \
&& mv objects.json _objects_test.json \
&& echo "::endgroup::"

## Clean up after quartodoc build
quartodoc_post: $(PYBIN)
. $(PYBIN)/activate \
Expand Down
4 changes: 3 additions & 1 deletion docs/_combine_objects_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ def write_objects_file(objects: QuartodocObject, path: str) -> None:
print("\nCombining objects json files...")
objects_core = read_objects_file("_objects_core.json")
objects_express = read_objects_file("_objects_express.json")
objects_test = read_objects_file("_objects_test.json")

items_map: dict[str, QuartodocObjectItem] = {}

for item in [*objects_core.items, *objects_express.items]:
for item in [*objects_core.items, *objects_express.items, *objects_test.items]:
if item.name in items_map:
continue
items_map[item.name] = item
Expand All @@ -58,6 +59,7 @@ def write_objects_file(objects: QuartodocObject, path: str) -> None:

print("Core:", objects_core.count)
print("Express:", objects_express.count)
print("Testing:", objects_test.count)
print("Combined:", objects_ret.count)

# Save combined objects file info
Expand Down
2 changes: 2 additions & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ website:
file: api/express/index.qmd
- text: "Core API"
file: api/core/index.qmd
- text: "Testing API"
file: api/test/index.qmd
right:
- icon: github
href: https://github.com/posit-dev/py-shiny
Expand Down
93 changes: 93 additions & 0 deletions docs/_quartodoc-testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
quartodoc:
style: pkgdown
dir: api/testing
out_index: index.qmd
package: shiny
rewrite_all_pages: false
sidebar: api/testing/_sidebar.yml
dynamic: false
renderer:
style: _renderer.py
show_signature_annotations: false
sections:
- title: UI Layouts
desc: Methods for interacting with Shiny app multiple UI component controls.
contents:
- playwright.controls.Accordion
- playwright.controls.AccordionPanel
- playwright.controls.Card
- playwright.controls.Popover
- playwright.controls.Sidebar
- playwright.controls.Tooltip
- title: UI Inputs
desc: Methods for interacting with Shiny app input value controls.
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
- title: Value boxes
desc: Methods for interacting with Shiny app valuebox controls.
contents:
- playwright.controls.ValueBox
- title: Navigation (tab) panels
desc: Methods for interacting with Shiny app UI content controls.
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
- title: Upload and download
desc: Methods for interacting with Shiny app uploading and downloading controls.
contents:
- playwright.controls.DownloadButton
- playwright.controls.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
- title: "Playwright Expect"
desc: "Methods for testing the state of a locator within a Shiny app."
contents:
- playwright.expect.expect_to_change
- playwright.expect.expect_attribute_to_have_value
- playwright.expect.expect_to_have_class
- playwright.expect.expect_not_to_have_class
- playwright.expect.expect_to_have_style
- title: "Pytest"
desc: "Fixtures used for testing Shiny apps with Pytest."
contents:
- pytest.create_app_fixture
- pytest.ScopeName
- title: "Run"
desc: "Methods for starting a local Shiny app in the background"
contents:
- run.run_shiny_app
- run.ShinyAppProc
39 changes: 39 additions & 0 deletions shiny/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,45 @@ def try_import_module(module: str) -> Optional[types.ModuleType]:
}


@main.group(help="""Add files to enhance your Shiny app.""")
def add() -> None:
pass


@add.command(
help="""Add a test file for a specified Shiny app.

Add an empty test file for a specified app. You will be prompted with a destination
folder. If you don't provide a destination folder, it will be added in the current
working directory based on the app name.

After creating the shiny app file, you can use `pytest` to run the tests:

pytest TEST_FILE
"""
)
@click.option(
"--app",
"-a",
type=str,
help="Please provide the path to the app file for which you want to create a test file.",
)
@click.option(
"--test-file",
"-t",
type=str,
help="Please provide the name of the test file you want to create. The basename of the test file should start with `test_` and be unique across all test files.",
)
# Param for app.py, param for test_name
def test(
app: Path | None,
test_file: Path | None,
) -> None:
from ._template_utils import add_test_file

add_test_file(app_file=app, test_file=test_file)


@main.command(
help="""Create a Shiny application from a template.

Expand Down
108 changes: 108 additions & 0 deletions shiny/_template_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,111 @@ def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = app_dir)
(app_dir / "app-core.py").rename(app_dir / "app.py")

return app_dir


def add_test_file(
*,
app_file: Path | None,
test_file: Path | None,
):

if app_file is None:

def path_exists(x: Path) -> bool | str:
if not isinstance(x, (str, Path)):
return False
if Path(x).is_dir():
return "Please provide a file path to your Shiny app"
return Path(x).exists() or f"Shiny app file can not be found: {x}"

app_file_val = questionary.path(
"Enter the path to the app file:",
default=build_path_string("app.py"),
validate=path_exists,
).ask()
else:
app_file_val = app_file
# User quit early
if app_file_val is None:
sys.exit(1)
app_file = Path(app_file_val)

if test_file is None:

def path_does_not_exist(x: Path) -> bool | str:
if not isinstance(x, (str, Path)):
return False
if Path(x).is_dir():
return "Please provide a file path for your test file."
if Path(x).exists():
return "Test file already exists. Please provide a new file name."
if not Path(x).name.startswith("test_"):
return "Test file must start with 'test_'"
return True

test_file_val = questionary.path(
"Enter the path to the test file:",
default=build_path_string(
os.path.relpath(app_file.parent / "tests" / "test_app.py", ".")
),
validate=path_does_not_exist,
).ask()
else:
test_file_val = test_file

# User quit early
if test_file_val is None:
sys.exit(1)
test_file = Path(test_file_val)

# Make sure app file exists
if not app_file.exists():
raise FileExistsError("App file does not exist: ", test_file)
# Make sure output test file doesn't exist
if test_file.exists():
raise FileExistsError("Test file already exists: ", test_file)
if not test_file.name.startswith("test_"):
return "Test file must start with 'test_'"

# if app path directory is the same as the test file directory, use `local_app`
# otherwise, use `create_app_fixture`
is_same_dir = app_file.parent == test_file.parent

test_name = test_file.name.replace(".py", "")
rel_path = os.path.relpath(app_file, test_file.parent)

template = (
f"""\
from playwright.sync_api import Page

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


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

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

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

app = create_app_fixture("{rel_path}")


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

page.goto(app.url)
# Add tests code here
"""
)
# Make sure test file directory exists
test_file.parent.mkdir(parents=True, exist_ok=True)

# Write template to test file
test_file.write_text(template)
2 changes: 2 additions & 0 deletions shiny/playwright/controls/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ._controls import (
Accordion,
AccordionPanel,
Card,
DownloadButton,
DownloadLink,
Expand Down Expand Up @@ -77,6 +78,7 @@
"ValueBox",
"Card",
"Accordion",
"AccordionPanel",
"Sidebar",
"Popover",
"Tooltip",
Expand Down
Loading