From a6efff92e5d5b965a8073bff8656c574f0c6ac48 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Mon, 10 Jun 2024 12:49:46 -0700 Subject: [PATCH 01/13] Add quarto page for shiny testing --- docs/_quartodoc-core.yml | 140 +++++++++++++++++++ shiny/playwright/expect/_expect.py | 10 +- shiny/playwright/expect/_expect_to_change.py | 6 +- shiny/pytest/_fixture.py | 4 +- shiny/run/_run.py | 3 +- 5 files changed, 154 insertions(+), 9 deletions(-) diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index a9c7c12f3..590247104 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -214,6 +214,145 @@ quartodoc: # uses class.rst template - module.ui - module.server + - title: Testing + desc: "" + contents: + - kind: page + path: PlaywrightControls + flatten: true + summary: + name: "Playwright Controls" + desc: "Methods for interacting with Shiny app controls." + contents: + - name: playwright.controls.Accordion + dynamic: false + - name: playwright.controls.Card + dynamic: false + - name: playwright.controls.DownloadButton + dynamic: false + - name: playwright.controls.DownloadLink + dynamic: false + - name: playwright.controls.InputActionButton + dynamic: false + - name: playwright.controls.InputActionLink + dynamic: false + - name: playwright.controls.InputCheckbox + dynamic: false + - name: playwright.controls.InputCheckboxGroup + dynamic: false + - name: playwright.controls.InputDarkMode + dynamic: false + - name: playwright.controls.InputDate + dynamic: false + - name: playwright.controls.InputDateRange + dynamic: false + - name: playwright.controls.InputFile + dynamic: false + - name: playwright.controls.InputNumeric + dynamic: false + - name: playwright.controls.InputPassword + dynamic: false + - name: playwright.controls.InputRadioButtons + dynamic: false + - name: playwright.controls.InputSelect + dynamic: false + - name: playwright.controls.InputSelectize + dynamic: false + - name: playwright.controls.InputSlider + dynamic: false + - name: playwright.controls.InputSliderRange + dynamic: false + - name: playwright.controls.InputSwitch + dynamic: false + - name: playwright.controls.InputTaskButton + dynamic: false + - name: playwright.controls.InputText + dynamic: false + - name: playwright.controls.InputTextArea + dynamic: false + - name: playwright.controls.NavItem + dynamic: false + - name: playwright.controls.NavsetBar + dynamic: false + - name: playwright.controls.NavsetCardPill + dynamic: false + - name: playwright.controls.NavsetCardTab + dynamic: false + - name: playwright.controls.NavsetCardUnderline + dynamic: false + - name: playwright.controls.NavsetHidden + dynamic: false + - name: playwright.controls.NavsetPill + dynamic: false + - name: playwright.controls.NavsetPillList + dynamic: false + - name: playwright.controls.NavsetTab + dynamic: false + - name: playwright.controls.NavsetUnderline + dynamic: false + - name: playwright.controls.OutputCode + dynamic: false + - name: playwright.controls.OutputDataFrame + dynamic: false + - name: playwright.controls.OutputImage + dynamic: false + - name: playwright.controls.OutputPlot + dynamic: false + - name: playwright.controls.OutputTable + dynamic: false + - name: playwright.controls.OutputText + dynamic: false + - name: playwright.controls.OutputTextVerbatim + dynamic: false + - name: playwright.controls.OutputUi + dynamic: false + - name: playwright.controls.Popover + dynamic: false + - name: playwright.controls.Sidebar + dynamic: false + - name: playwright.controls.Tooltip + dynamic: false + - name: playwright.controls.ValueBox + dynamic: false + - kind: page + path: PlaywrightExpect + flatten: true + summary: + name: "Playwright Expect" + desc: "Methods for testing the state of a Shiny app." + contents: + - name: playwright.expect.expect_to_change + dynamic: false + - name: playwright.expect.expect_attribute_to_have_value + dynamic: false + - name: playwright.expect.expect_to_have_class + dynamic: false + - name: playwright.expect.expect_not_to_have_class + dynamic: false + - name: playwright.expect.expect_to_have_style + dynamic: false + - kind: page + path: Pytest + flatten: true + summary: + name: "Pytest" + desc: "Fixtures used for testing Shiny apps with Pytest." + contents: + - name: pytest.create_app_fixture + dynamic: false + - name: pytest.ScopeName + dynamic: false + - kind: page + path: Run + flatten: true + summary: + name: "Run" + desc: "Methods for running a Shiny app locally on a machine" + contents: + - name: run.run_shiny_app + dynamic: false + - name: run.ShinyAppProc + dynamic: false - title: Developer facing tools desc: "" contents: @@ -331,6 +470,7 @@ quartodoc: - types.SilentException - types.SilentCancelOutputException - types.SafeException + - title: Deprecated desc: "" contents: diff --git a/shiny/playwright/expect/_expect.py b/shiny/playwright/expect/_expect.py index 89734bc94..d44687f81 100644 --- a/shiny/playwright/expect/_expect.py +++ b/shiny/playwright/expect/_expect.py @@ -11,6 +11,8 @@ from ..._typing_extensions import assert_type from .._types import AttrValue, PatternOrStr, PatternStr, StyleValue, Timeout +from ..._docstring import no_example + # Internal method only! # "_expect_class_value", __all__ = ( @@ -20,7 +22,7 @@ "expect_to_have_style", ) - +@no_example() def expect_attribute_to_have_value( loc: Locator, name: str, @@ -38,7 +40,7 @@ def expect_attribute_to_have_value( playwright_expect(loc).to_have_attribute(name=name, value=value, timeout=timeout) - +@no_example() def expect_to_have_class( loc: Locator, cls: str, @@ -48,7 +50,7 @@ def expect_to_have_class( cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)") playwright_expect(loc).to_have_class(cls_regex, timeout=timeout) - +@no_example() def expect_not_to_have_class( loc: Locator, cls: str, @@ -58,7 +60,7 @@ def expect_not_to_have_class( cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)") playwright_expect(loc).not_to_have_class(cls_regex, timeout=timeout) - +@no_example() def expect_to_have_style( loc: Locator, css_key: str, diff --git a/shiny/playwright/expect/_expect_to_change.py b/shiny/playwright/expect/_expect_to_change.py index a5800095d..c2d631a7c 100644 --- a/shiny/playwright/expect/_expect_to_change.py +++ b/shiny/playwright/expect/_expect_to_change.py @@ -2,10 +2,10 @@ import time from contextlib import contextmanager from typing import Any, Callable, Generator - +from ..._docstring import no_example __all__ = ("expect_to_change",) - +@no_example() @contextmanager def expect_to_change( func: Callable[[], Any], timeout_secs: float = 10 @@ -47,7 +47,7 @@ def wait_for_change(): wait_for_change() - +@no_example() def retry_with_timeout(timeout: float = 30): """ Decorator that retries a function until 1) it succeeds, 2) fails with a diff --git a/shiny/pytest/_fixture.py b/shiny/pytest/_fixture.py index 9732fa53d..24fea37d3 100644 --- a/shiny/pytest/_fixture.py +++ b/shiny/pytest/_fixture.py @@ -6,6 +6,8 @@ import pytest from ..run._run import shiny_app_gen +from ..._docstring import no_example + __all__ = ( "create_app_fixture", @@ -15,7 +17,7 @@ ScopeName = Literal["session", "package", "module", "class", "function"] - +@no_example() def create_app_fixture( app: Union[PurePath, str], scope: ScopeName = "module", diff --git a/shiny/run/_run.py b/shiny/run/_run.py index c07c6a532..617b9feaa 100644 --- a/shiny/run/_run.py +++ b/shiny/run/_run.py @@ -10,6 +10,7 @@ from typing import IO, Any, Callable, Generator, List, Optional, TextIO, Type, Union from .._utils import random_port +from .._docstring import no_example __all__ = ( "ShinyAppProc", @@ -199,7 +200,7 @@ def stderr_uvicorn(line: str) -> bool: + "\n".join(error_lines[-20:]) ) - +@no_example() def run_shiny_app( app_file: Union[str, PurePath], *, From 24b2b231202c95c52357445f149ba85e593307ce Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Mon, 10 Jun 2024 12:51:19 -0700 Subject: [PATCH 02/13] linting changes --- shiny/playwright/expect/_expect.py | 7 +++++-- shiny/playwright/expect/_expect_to_change.py | 4 ++++ shiny/pytest/_fixture.py | 4 ++-- shiny/run/_run.py | 3 ++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/shiny/playwright/expect/_expect.py b/shiny/playwright/expect/_expect.py index d44687f81..dd83a6899 100644 --- a/shiny/playwright/expect/_expect.py +++ b/shiny/playwright/expect/_expect.py @@ -8,11 +8,10 @@ from playwright.sync_api import Locator from playwright.sync_api import expect as playwright_expect +from ..._docstring import no_example from ..._typing_extensions import assert_type from .._types import AttrValue, PatternOrStr, PatternStr, StyleValue, Timeout -from ..._docstring import no_example - # Internal method only! # "_expect_class_value", __all__ = ( @@ -22,6 +21,7 @@ "expect_to_have_style", ) + @no_example() def expect_attribute_to_have_value( loc: Locator, @@ -40,6 +40,7 @@ def expect_attribute_to_have_value( playwright_expect(loc).to_have_attribute(name=name, value=value, timeout=timeout) + @no_example() def expect_to_have_class( loc: Locator, @@ -50,6 +51,7 @@ def expect_to_have_class( cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)") playwright_expect(loc).to_have_class(cls_regex, timeout=timeout) + @no_example() def expect_not_to_have_class( loc: Locator, @@ -60,6 +62,7 @@ def expect_not_to_have_class( cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)") playwright_expect(loc).not_to_have_class(cls_regex, timeout=timeout) + @no_example() def expect_to_have_style( loc: Locator, diff --git a/shiny/playwright/expect/_expect_to_change.py b/shiny/playwright/expect/_expect_to_change.py index c2d631a7c..284b37d02 100644 --- a/shiny/playwright/expect/_expect_to_change.py +++ b/shiny/playwright/expect/_expect_to_change.py @@ -2,9 +2,12 @@ import time from contextlib import contextmanager from typing import Any, Callable, Generator + from ..._docstring import no_example + __all__ = ("expect_to_change",) + @no_example() @contextmanager def expect_to_change( @@ -47,6 +50,7 @@ def wait_for_change(): wait_for_change() + @no_example() def retry_with_timeout(timeout: float = 30): """ diff --git a/shiny/pytest/_fixture.py b/shiny/pytest/_fixture.py index 24fea37d3..e27b5029e 100644 --- a/shiny/pytest/_fixture.py +++ b/shiny/pytest/_fixture.py @@ -5,9 +5,8 @@ import pytest -from ..run._run import shiny_app_gen from ..._docstring import no_example - +from ..run._run import shiny_app_gen __all__ = ( "create_app_fixture", @@ -17,6 +16,7 @@ ScopeName = Literal["session", "package", "module", "class", "function"] + @no_example() def create_app_fixture( app: Union[PurePath, str], diff --git a/shiny/run/_run.py b/shiny/run/_run.py index 617b9feaa..6323c79e3 100644 --- a/shiny/run/_run.py +++ b/shiny/run/_run.py @@ -9,8 +9,8 @@ from types import TracebackType from typing import IO, Any, Callable, Generator, List, Optional, TextIO, Type, Union -from .._utils import random_port from .._docstring import no_example +from .._utils import random_port __all__ = ( "ShinyAppProc", @@ -200,6 +200,7 @@ def stderr_uvicorn(line: str) -> bool: + "\n".join(error_lines[-20:]) ) + @no_example() def run_shiny_app( app_file: Union[str, PurePath], From f1a4355a403b6bc39843dbb2c903642622bdabf9 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 11 Jun 2024 10:59:23 -0700 Subject: [PATCH 03/13] Add description --- docs/_quartodoc-core.yml | 2 +- shiny/pytest/_fixture.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index 590247104..058809f51 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -215,7 +215,7 @@ quartodoc: - module.ui - module.server - title: Testing - desc: "" + desc: "Testing Methods to make it easier to run and test user's Shiny apps." contents: - kind: page path: PlaywrightControls diff --git a/shiny/pytest/_fixture.py b/shiny/pytest/_fixture.py index e27b5029e..e0c3c6df2 100644 --- a/shiny/pytest/_fixture.py +++ b/shiny/pytest/_fixture.py @@ -5,7 +5,7 @@ import pytest -from ..._docstring import no_example +from .._docstring import no_example from ..run._run import shiny_app_gen __all__ = ( From dccc18c1020c85c574f0e027b28e0225d918b278 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 11 Jun 2024 11:16:09 -0700 Subject: [PATCH 04/13] modify the desc for some --- docs/_quartodoc-core.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index 058809f51..5372699aa 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -319,7 +319,7 @@ quartodoc: flatten: true summary: name: "Playwright Expect" - desc: "Methods for testing the state of a Shiny app." + desc: "Methods for testing the state of a locator within a Shiny app." contents: - name: playwright.expect.expect_to_change dynamic: false @@ -347,7 +347,7 @@ quartodoc: flatten: true summary: name: "Run" - desc: "Methods for running a Shiny app locally on a machine" + desc: "Methods for starting a local Shiny app in the background" contents: - name: run.run_shiny_app dynamic: false From d097695f205efeaef348dcf376cc1b1769d1e81f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 11 Jun 2024 15:16:20 -0400 Subject: [PATCH 05/13] Code review --- docs/_quartodoc-core.yml | 2 + shiny/playwright/controls/__init__.py | 2 + shiny/playwright/controls/_controls.py | 89 +++++++++++++------------- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index 5372699aa..e4cfa2726 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -226,6 +226,8 @@ quartodoc: contents: - name: playwright.controls.Accordion dynamic: false + - name: playwright.controls.AccordionPanel + dynamic: false - name: playwright.controls.Card dynamic: false - name: playwright.controls.DownloadButton diff --git a/shiny/playwright/controls/__init__.py b/shiny/playwright/controls/__init__.py index 6345cdd66..a17b0eb52 100644 --- a/shiny/playwright/controls/__init__.py +++ b/shiny/playwright/controls/__init__.py @@ -1,5 +1,6 @@ from ._controls import ( Accordion, + AccordionPanel, Card, DownloadButton, DownloadLink, @@ -77,6 +78,7 @@ "ValueBox", "Card", "Accordion", + "AccordionPanel", "Sidebar", "Popover", "Tooltip", diff --git a/shiny/playwright/controls/_controls.py b/shiny/playwright/controls/_controls.py index a86cd721b..5523c9c30 100644 --- a/shiny/playwright/controls/_controls.py +++ b/shiny/playwright/controls/_controls.py @@ -419,7 +419,7 @@ class InputNumeric( _WidthLocM, _UiWithLabel, ): - """Input numeric control for :func:`~shiny.ui.input_numeric`.""" + """Controller for :func:`shiny.ui.input_numeric`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -573,7 +573,7 @@ class InputText( _ExpectSpellcheckAttrM, _UiWithLabel, ): - """Input text control for :func:`~shiny.ui.input_text`.""" + """Controller for :func:`shiny.ui.input_text`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -599,7 +599,7 @@ class InputPassword( _ExpectPlaceholderAttrM, _UiWithLabel, ): - """Input password control for :func:`~shiny.ui.input_password`.""" + """Controller for :func:`shiny.ui.input_password`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -650,7 +650,7 @@ class InputTextArea( _ExpectSpellcheckAttrM, _UiWithLabel, ): - """Input text area control for :func:`~shiny.ui.input_text_area`.""" + """Controller for :func:`shiny.ui.input_text_area`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -990,7 +990,7 @@ def expect_size(self, value: AttrValue, *, timeout: Timeout = None) -> None: class InputSelect(_InputSelectBase): - """Input select control for :func:`~shiny.ui.input_select`.""" + """Controller for :func:`shiny.ui.input_select`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -1031,7 +1031,7 @@ def expect_selectize(self, value: bool, *, timeout: Timeout = None) -> None: class InputSelectize(_InputSelectBase): - """Input selectize control for :func:`~shiny.ui.input_selectize`.""" + """Controller for :func:`shiny.ui.input_selectize`.""" def __init__(self, page: Page, id: str) -> None: super().__init__( @@ -1079,7 +1079,7 @@ class InputActionButton( _WidthLocM, _InputActionBase, ): - """Input action button control for :func:`~shiny.ui.input_action_button`.""" + """Controller for :func:`shiny.ui.input_action_button`.""" def __init__( self, @@ -1104,7 +1104,7 @@ def __init__( class InputDarkMode(_UiBase): - """Input dark mode control for :func:`~shiny.ui.input_dark_mode`.""" + """Controller for :func:`shiny.ui.input_dark_mode`.""" def __init__( self, @@ -1193,7 +1193,7 @@ class InputTaskButton( _WidthLocM, _InputActionBase, ): - """Input task button control for :func:`~shiny.ui.input_task_button`.""" + """Controller for :func:`shiny.ui.input_task_button`.""" # TODO-Karan: Test auto_reset functionality def __init__( @@ -1322,7 +1322,7 @@ def expect_auto_reset(self, value: bool, timeout: Timeout = None): class InputActionLink(_InputActionBase): - """Input action link control for :func:`~shiny.ui.input_action_link`.""" + """Controller for :func:`shiny.ui.input_action_link`.""" def __init__( self, @@ -1427,7 +1427,7 @@ def expect_checked(self, value: bool, *, timeout: Timeout = None) -> None: class InputCheckbox(_InputCheckboxBase): - """Input checkbox control for :func:`~shiny.ui.input_checkbox`.""" + """Controller for :func:`shiny.ui.input_checkbox`.""" def __init__( self, @@ -1453,7 +1453,7 @@ def __init__( class InputSwitch(_InputCheckboxBase): - """Input switch control for :func:`~shiny.ui.input_switch`.""" + """Controller for :func:`shiny.ui.input_switch`.""" def __init__( self, @@ -1925,7 +1925,7 @@ class InputRadioButtons( _WidthContainerM, _RadioButtonCheckboxGroupBase, ): - """Input radio buttons control for :func:`~shiny.ui.input_radio_buttons`.""" + """Controller for :func:`shiny.ui.input_radio_buttons`.""" loc_selected: Locator """ @@ -2058,7 +2058,7 @@ class InputFile( # _ExpectPlaceholderAttrM, _UiWithLabel, ): - """Input file control for :func:`~shiny.ui.input_file`.""" + """Controller for :func:`shiny.ui.input_file`.""" loc_button: Locator """ @@ -2665,7 +2665,7 @@ def _handle_center( class InputSlider(_InputSliderBase): - """Input slider control for :func:`~shiny.ui.input_slider`.""" + """Controller for :func:`shiny.ui.input_slider`.""" loc_irs_label: Locator """ @@ -2740,7 +2740,7 @@ def set( class InputSliderRange(_InputSliderBase): - """Input slider range control for :func:`~shiny.ui.input_slider_range`.""" + """Controller for :func:`shiny.ui.input_slider_range`.""" loc_irs_label_from: Locator """ @@ -3150,7 +3150,7 @@ def __init__(self, page: Page, id: str) -> None: class InputDateRange(_WidthContainerM, _UiWithLabel): - """Input date range control for :func:`~shiny.ui.input_date_range`.""" + """Controller for :func:`shiny.ui.input_date_range`.""" loc_separator: Locator """ @@ -3565,7 +3565,7 @@ class OutputText( _OutputInlineContainerM, _OutputTextValue, ): - """Text output control for :func:`~shiny.ui.text_output`.""" + """Controller for :func:`shiny.ui.text_output`.""" loc: Locator """ @@ -3602,7 +3602,7 @@ def get_value(self, *, timeout: Timeout = None) -> str: class OutputCode(_OutputTextValue): - """Code output control for :func:`~shiny.ui.code_output`.""" + """Controller for :func:`shiny.ui.code_output`.""" loc: Locator """ @@ -3647,7 +3647,7 @@ def expect_has_placeholder( class OutputTextVerbatim(_OutputTextValue): - """Verbatim text output control for :func:`~shiny.ui.text_output_verbatim`.""" + """Controller for :func:`shiny.ui.text_output_verbatim`.""" loc: Locator """ @@ -3851,7 +3851,7 @@ def __init__(self, page: Page, id: str) -> None: class OutputPlot(_OutputImageBase): - """Plot output control for :func:`~shiny.ui.plot_output`.""" + """Controller for :func:`shiny.ui.plot_output`.""" loc: Locator """ @@ -3873,7 +3873,7 @@ def __init__(self, page: Page, id: str) -> None: class OutputUi(_OutputInlineContainerM, _OutputBase): - """UI output control for :func:`~shiny.ui.ui_output`.""" + """Controller for :func:`shiny.ui.ui_output`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -3922,7 +3922,7 @@ def expect_text(self, value: str, *, timeout: Timeout = None) -> None: # When making selectors, use `xpath` so that direct decendents can be checked class OutputTable(_OutputBase): - """Table output control for :func:`~shiny.ui.table_output`.""" + """Controller for :func:`shiny.ui.table_output`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -4073,7 +4073,7 @@ class Sidebar( _WidthLocM, _UiWithContainer, ): - """Sidebar control for func: `~shiny.ui.sidebar`.""" + """Controller for func: `shiny.ui.sidebar`.""" loc_container: Locator """ @@ -4375,7 +4375,7 @@ class ValueBox( _UiWithContainer, ): """ - Value Box control for :func:`~shiny.ui.value_box`. + Controller for :func:`shiny.ui.value_box`. """ loc: Locator @@ -4520,7 +4520,7 @@ class Card( _UiWithContainer, ): """ - Card control for :func:`~shiny.ui.card`. + Controller for :func:`shiny.ui.card`. """ loc_container: Locator @@ -4677,7 +4677,7 @@ class Accordion( _WidthLocM, _UiWithContainer, ): - """Accordion control for :func:`~shiny.ui.accordion`.""" + """Controller for :func:`shiny.ui.accordion`.""" loc: Locator """ @@ -4825,7 +4825,8 @@ def accordion_panel( data_value: str, ) -> AccordionPanel: """ - Returns the accordion panel with the specified data value. + Returns the accordion panel (:class:`~shiny.playwright.controls.AccordionPanel`) + with the specified data value. Parameters ---------- @@ -4840,7 +4841,7 @@ class AccordionPanel( _UiWithContainer, ): """ - AccordionPanel control for :func:`~shiny.ui.accordion_panel`. + Controller for :func:`shiny.ui.accordion_panel`. """ loc_label: Locator @@ -5119,7 +5120,7 @@ def expect_placement(self, value: str, *, timeout: Timeout = None) -> None: class Popover(_OverlayBase): - """Popover control for :func:`~shiny.ui.popover`.""" + """Controller for :func:`shiny.ui.popover`.""" loc_trigger: Locator """ @@ -5182,7 +5183,7 @@ def toggle(self, timeout: Timeout = None) -> None: class Tooltip(_OverlayBase): - """Tooltip control for :func:`~shiny.ui.tooltip`.""" + """Controller for :func:`shiny.ui.tooltip`.""" loc_container: Locator """ @@ -5374,7 +5375,7 @@ def expect_nav_titles( class NavItem(_UiWithContainer): - """Navigation item control for :func:`~shiny.ui.nav_item`.""" + """Controller for :func:`shiny.ui.nav_item`.""" """ Playwright `Locator` for the content of the nav item. @@ -5463,7 +5464,7 @@ def expect_content(self, value: PatternOrStr, *, timeout: Timeout = None) -> Non class NavsetTab(_NavItemBase): - """NavsetTab control for :func:`~shiny.ui.navset_tab`.""" + """Controller for :func:`shiny.ui.navset_tab`.""" loc: Locator """ @@ -5494,7 +5495,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetPill(_NavItemBase): - """NavsetPill control for :func:`~shiny.ui.navset_pill`.""" + """Controller for :func:`shiny.ui.navset_pill`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5516,7 +5517,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetUnderline(_NavItemBase): - """NavsetUnderline control for :func:`~shiny.ui.navset_underline`.""" + """Controller for :func:`shiny.ui.navset_underline`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5538,7 +5539,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetPillList(_NavItemBase): - """NavsetPillList control for :func:`~shiny.ui.navset_pill_list`.""" + """Controller for :func:`shiny.ui.navset_pill_list`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5560,7 +5561,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetCardTab(_NavItemBase): - """NavsetCardTab control for :func:`~shiny.ui.navset_card_tab`.""" + """Controller for :func:`shiny.ui.navset_card_tab`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5582,7 +5583,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetCardPill(_NavItemBase): - """NavsetCardPill control for :func:`~shiny.ui.navset_card_pill`.""" + """Controller for :func:`shiny.ui.navset_card_pill`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5604,7 +5605,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetCardUnderline(_NavItemBase): - """NavsetCardUnderline control for :func:`~shiny.ui.navset_card_underline`.""" + """Controller for :func:`shiny.ui.navset_card_underline`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5626,7 +5627,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetHidden(_NavItemBase): - """NavsetHidden control for :func:`~shiny.ui.navset_hidden`.""" + """Controller for :func:`shiny.ui.navset_hidden`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5648,7 +5649,7 @@ def __init__(self, page: Page, id: str) -> None: class NavsetBar(_NavItemBase): - """NavsetBar control for :func:`~shiny.ui.navset_bar`.""" + """Controller for :func:`shiny.ui.navset_bar`.""" def __init__(self, page: Page, id: str) -> None: """ @@ -5671,7 +5672,7 @@ def __init__(self, page: Page, id: str) -> None: class OutputDataFrame(_UiWithContainer): """ - OutputDataFrame control for :func:`~shiny.ui.output_data_frame`. + Controller for :func:`shiny.ui.output_data_frame`. """ loc_container: Locator @@ -6144,7 +6145,7 @@ def expect_cell_title( # TODO: Use mixin for dowloadlink and download button class DownloadLink(_InputActionBase): """ - DownloadLink control for :func:`~shiny.ui.download_link`. + Controller for :func:`shiny.ui.download_link`. """ def __init__(self, page: Page, id: str) -> None: @@ -6170,7 +6171,7 @@ class DownloadButton( _InputActionBase, ): """ - DownloadButton control for :func:`~shiny.ui.download_button` + Controller for :func:`shiny.ui.download_button` """ def __init__(self, page: Page, id: str) -> None: From 0e6e5e3aeeb69e12877d9064ac1bc00f45448bb7 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Mon, 17 Jun 2024 09:13:38 -0700 Subject: [PATCH 06/13] Create a new page for testing API --- docs/Makefile | 2 +- docs/_combine_objects_json.py | 4 +- docs/_quarto.yml | 2 + docs/_quartodoc-core.yml | 141 ----------------------------- docs/_quartodoc_testing.yml | 166 ++++++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 143 deletions(-) create mode 100644 docs/_quartodoc_testing.yml diff --git a/docs/Makefile b/docs/Makefile index 4dbb4e1fa..1849709c0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -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) diff --git a/docs/_combine_objects_json.py b/docs/_combine_objects_json.py index fbb2a2215..f16636409 100644 --- a/docs/_combine_objects_json.py +++ b/docs/_combine_objects_json.py @@ -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 @@ -58,6 +59,7 @@ def write_objects_file(objects: QuartodocObject, path: str) -> None: print("Core:", objects_core.count) print("Express:", objects_express.count) +print("Test:", objects_test.count) print("Combined:", objects_ret.count) # Save combined objects file info diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 2d6b65308..eabda5673 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -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 diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index e4cfa2726..f71960bba 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -214,147 +214,6 @@ quartodoc: # uses class.rst template - module.ui - module.server - - title: Testing - desc: "Testing Methods to make it easier to run and test user's Shiny apps." - contents: - - kind: page - path: PlaywrightControls - flatten: true - summary: - name: "Playwright Controls" - desc: "Methods for interacting with Shiny app controls." - contents: - - name: playwright.controls.Accordion - dynamic: false - - name: playwright.controls.AccordionPanel - dynamic: false - - name: playwright.controls.Card - dynamic: false - - name: playwright.controls.DownloadButton - dynamic: false - - name: playwright.controls.DownloadLink - dynamic: false - - name: playwright.controls.InputActionButton - dynamic: false - - name: playwright.controls.InputActionLink - dynamic: false - - name: playwright.controls.InputCheckbox - dynamic: false - - name: playwright.controls.InputCheckboxGroup - dynamic: false - - name: playwright.controls.InputDarkMode - dynamic: false - - name: playwright.controls.InputDate - dynamic: false - - name: playwright.controls.InputDateRange - dynamic: false - - name: playwright.controls.InputFile - dynamic: false - - name: playwright.controls.InputNumeric - dynamic: false - - name: playwright.controls.InputPassword - dynamic: false - - name: playwright.controls.InputRadioButtons - dynamic: false - - name: playwright.controls.InputSelect - dynamic: false - - name: playwright.controls.InputSelectize - dynamic: false - - name: playwright.controls.InputSlider - dynamic: false - - name: playwright.controls.InputSliderRange - dynamic: false - - name: playwright.controls.InputSwitch - dynamic: false - - name: playwright.controls.InputTaskButton - dynamic: false - - name: playwright.controls.InputText - dynamic: false - - name: playwright.controls.InputTextArea - dynamic: false - - name: playwright.controls.NavItem - dynamic: false - - name: playwright.controls.NavsetBar - dynamic: false - - name: playwright.controls.NavsetCardPill - dynamic: false - - name: playwright.controls.NavsetCardTab - dynamic: false - - name: playwright.controls.NavsetCardUnderline - dynamic: false - - name: playwright.controls.NavsetHidden - dynamic: false - - name: playwright.controls.NavsetPill - dynamic: false - - name: playwright.controls.NavsetPillList - dynamic: false - - name: playwright.controls.NavsetTab - dynamic: false - - name: playwright.controls.NavsetUnderline - dynamic: false - - name: playwright.controls.OutputCode - dynamic: false - - name: playwright.controls.OutputDataFrame - dynamic: false - - name: playwright.controls.OutputImage - dynamic: false - - name: playwright.controls.OutputPlot - dynamic: false - - name: playwright.controls.OutputTable - dynamic: false - - name: playwright.controls.OutputText - dynamic: false - - name: playwright.controls.OutputTextVerbatim - dynamic: false - - name: playwright.controls.OutputUi - dynamic: false - - name: playwright.controls.Popover - dynamic: false - - name: playwright.controls.Sidebar - dynamic: false - - name: playwright.controls.Tooltip - dynamic: false - - name: playwright.controls.ValueBox - dynamic: false - - kind: page - path: PlaywrightExpect - flatten: true - summary: - name: "Playwright Expect" - desc: "Methods for testing the state of a locator within a Shiny app." - contents: - - name: playwright.expect.expect_to_change - dynamic: false - - name: playwright.expect.expect_attribute_to_have_value - dynamic: false - - name: playwright.expect.expect_to_have_class - dynamic: false - - name: playwright.expect.expect_not_to_have_class - dynamic: false - - name: playwright.expect.expect_to_have_style - dynamic: false - - kind: page - path: Pytest - flatten: true - summary: - name: "Pytest" - desc: "Fixtures used for testing Shiny apps with Pytest." - contents: - - name: pytest.create_app_fixture - dynamic: false - - name: pytest.ScopeName - dynamic: false - - kind: page - path: Run - flatten: true - summary: - name: "Run" - desc: "Methods for starting a local Shiny app in the background" - contents: - - name: run.run_shiny_app - dynamic: false - - name: run.ShinyAppProc - dynamic: false - title: Developer facing tools desc: "" contents: diff --git a/docs/_quartodoc_testing.yml b/docs/_quartodoc_testing.yml new file mode 100644 index 000000000..65198fcba --- /dev/null +++ b/docs/_quartodoc_testing.yml @@ -0,0 +1,166 @@ +quartodoc: + style: pkgdown + dir: api/testing + out_index: index.qmd + package: shiny + rewrite_all_pages: false + sidebar: api/testing/_sidebar.yml + dynamic: true + renderer: + style: _renderer.py + show_signature_annotations: false + sections: + - title: Testing + desc: "Testing Methods to make it easier to run and test user's Shiny apps." + contents: + - kind: page + path: PlaywrightControls + flatten: true + summary: + name: "Playwright Controls" + desc: "Methods for interacting with Shiny app controls." + - title: UI Layouts + desc: Methods for interacting with Shiny app multiple UI component controls. + contents: + - name: playwright.controls.Accordion + dynamic: false + - name: playwright.controls.AccordionPanel + dynamic: false + - name: playwright.controls.Card + dynamic: false + - name: playwright.controls.Popover + dynamic: false + - name: playwright.controls.Sidebar + dynamic: false + - name: playwright.controls.Tooltip + dynamic: false + - title: UI Inputs + desc: Methods for interacting with Shiny app input value controls. + contents: + - name: playwright.controls.InputActionLink + dynamic: false + - name: playwright.controls.InputCheckbox + dynamic: false + - name: playwright.controls.InputCheckboxGroup + dynamic: false + - name: playwright.controls.InputDarkMode + dynamic: false + - name: playwright.controls.InputDate + dynamic: false + - name: playwright.controls.InputDateRange + dynamic: false + - name: playwright.controls.InputFile + dynamic: false + - name: playwright.controls.InputNumeric + dynamic: false + - name: playwright.controls.InputPassword + dynamic: false + - name: playwright.controls.InputRadioButtons + dynamic: false + - name: playwright.controls.InputSelect + dynamic: false + - name: playwright.controls.InputSelectize + dynamic: false + - name: playwright.controls.InputSlider + dynamic: false + - name: playwright.controls.InputSliderRange + dynamic: false + - name: playwright.controls.InputSwitch + dynamic: false + - name: playwright.controls.InputTaskButton + dynamic: false + - name: playwright.controls.InputText + dynamic: false + - name: playwright.controls.InputTextArea + - title: Value boxes + desc: Methods for interacting with Shiny app valuebox controls. + contents: + - name: playwright.controls.ValueBox + dynamic: false + - title: Navigation (tab) panels + desc: Methods for interacting with Shiny app UI content controls. + contents: + - name: playwright.controls.NavItem + dynamic: false + - name: playwright.controls.NavsetBar + dynamic: false + - name: playwright.controls.NavsetCardPill + dynamic: false + - name: playwright.controls.NavsetCardTab + dynamic: false + - name: playwright.controls.NavsetCardUnderline + dynamic: false + - name: playwright.controls.NavsetHidden + dynamic: false + - name: playwright.controls.NavsetPill + dynamic: false + - name: playwright.controls.NavsetPillList + dynamic: false + - name: playwright.controls.NavsetTab + dynamic: false + - name: playwright.controls.NavsetUnderline + dynamic: false + - title: Upload and download + desc: Create UI for uploading and downloading files. + contents: + - name: playwright.controls.DownloadButton + dynamic: false + - name: playwright.controls.DownloadLink + dynamic: false + - title: Rendering outputs + desc: Render output in a variety of ways. + contents: + - name: playwright.controls.OutputCode + dynamic: false + - name: playwright.controls.OutputDataFrame + dynamic: false + - name: playwright.controls.OutputImage + dynamic: false + - name: playwright.controls.OutputPlot + dynamic: false + - name: playwright.controls.OutputTable + dynamic: false + - name: playwright.controls.OutputText + dynamic: false + - name: playwright.controls.OutputTextVerbatim + dynamic: false + - name: playwright.controls.OutputUi + - kind: page + path: PlaywrightExpect + flatten: true + summary: + name: "Playwright Expect" + desc: "Methods for testing the state of a locator within a Shiny app." + contents: + - name: playwright.expect.expect_to_change + dynamic: false + - name: playwright.expect.expect_attribute_to_have_value + dynamic: false + - name: playwright.expect.expect_to_have_class + dynamic: false + - name: playwright.expect.expect_not_to_have_class + dynamic: false + - name: playwright.expect.expect_to_have_style + dynamic: false + - kind: page + path: Pytest + flatten: true + summary: + name: "Pytest" + desc: "Fixtures used for testing Shiny apps with Pytest." + contents: + - name: pytest.create_app_fixture + dynamic: false + - name: pytest.ScopeName + dynamic: false + - kind: page + path: Run + flatten: true + summary: + name: "Run" + desc: "Methods for starting a local Shiny app in the background" + contents: + - name: run.run_shiny_app + dynamic: false + - name: run.ShinyAppProc + dynamic: false From 9ebb1cd0e54bf506b9cff5e07f916fb438b20c7e Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 18 Jun 2024 11:54:18 -0700 Subject: [PATCH 07/13] Add command in makefile for docs --- docs/Makefile | 8 ++++++ ...doc_testing.yml => _quartodoc-testing.yml} | 0 shiny/_main.py | 26 +++++++++++++++++++ shiny/_template_utils.py | 19 ++++++++++++++ 4 files changed, 53 insertions(+) rename docs/{_quartodoc_testing.yml => _quartodoc-testing.yml} (100%) diff --git a/docs/Makefile b/docs/Makefile index 1849709c0..5823bcd06 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -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 test docs" \ + && quartodoc build --config _quartodoc-testing.yml --verbose \ + && mv objects.json _objects_express.json \ + && echo "::endgroup::" + ## Clean up after quartodoc build quartodoc_post: $(PYBIN) . $(PYBIN)/activate \ diff --git a/docs/_quartodoc_testing.yml b/docs/_quartodoc-testing.yml similarity index 100% rename from docs/_quartodoc_testing.yml rename to docs/_quartodoc-testing.yml diff --git a/shiny/_main.py b/shiny/_main.py index cad3e0905..c23a32766 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -503,6 +503,32 @@ def try_import_module(module: str) -> Optional[types.ModuleType]: } +@main.command( + help="""Add a test file for a specified 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 APPDIR/test_APPNAME.py +""" +) +@click.option( + "--app", + "-a", + type=str, + help="Please provide the path to the app for which you want to create a test file.", +) +def add_tests( + app_dir: Path, +) -> None: + from ._template_utils import add_test_file + + add_test_file(app_dir) + + @main.command( help="""Create a Shiny application from a template. diff --git a/shiny/_template_utils.py b/shiny/_template_utils.py index e79047486..a3cfd1361 100644 --- a/shiny/_template_utils.py +++ b/shiny/_template_utils.py @@ -322,3 +322,22 @@ 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_dir: Path): + test_file = app_dir / "test_app.py" + test_file.write_text( +""" +from shiny.playwright.controls import +from shiny.run import ShinyAppProc +from playwright.sync_api import Page +from shiny.pytest import create_app_fixture + +app = create_app_fixture(f"{app_dir}/app.py") + + +def test_app(page: Page, app: ShinyAppProc): + page.goto(app.url) + # Add tests code here +""" + ) From 0b56c4814666da89c4cd6d8a4b0de258f09885f5 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 18 Jun 2024 13:39:25 -0700 Subject: [PATCH 08/13] make dynamic as false by default --- docs/_quartodoc-testing.yml | 244 +++++++++++++----------------------- shiny/_main.py | 1 + 2 files changed, 91 insertions(+), 154 deletions(-) diff --git a/docs/_quartodoc-testing.yml b/docs/_quartodoc-testing.yml index 65198fcba..400c3e1dc 100644 --- a/docs/_quartodoc-testing.yml +++ b/docs/_quartodoc-testing.yml @@ -5,162 +5,98 @@ quartodoc: package: shiny rewrite_all_pages: false sidebar: api/testing/_sidebar.yml - dynamic: true + dynamic: false renderer: style: _renderer.py show_signature_annotations: false sections: - - title: Testing - desc: "Testing Methods to make it easier to run and test user's Shiny apps." + # - title: Testing + # desc: "Testing Methods to make it easier to run and test user's Shiny apps." + # contents: + # - kind: page + # path: PlaywrightControls + # flatten: true + # summary: + # name: "Playwright Controls" + # desc: "Methods for interacting with Shiny app controls." + - title: UI Layouts + desc: Methods for interacting with Shiny app multiple UI component controls. contents: - - kind: page - path: PlaywrightControls - flatten: true - summary: - name: "Playwright Controls" - desc: "Methods for interacting with Shiny app controls." - - title: UI Layouts - desc: Methods for interacting with Shiny app multiple UI component controls. - contents: - - name: playwright.controls.Accordion - dynamic: false - - name: playwright.controls.AccordionPanel - dynamic: false - - name: playwright.controls.Card - dynamic: false - - name: playwright.controls.Popover - dynamic: false - - name: playwright.controls.Sidebar - dynamic: false - - name: playwright.controls.Tooltip - dynamic: false - - title: UI Inputs - desc: Methods for interacting with Shiny app input value controls. - contents: - - name: playwright.controls.InputActionLink - dynamic: false - - name: playwright.controls.InputCheckbox - dynamic: false - - name: playwright.controls.InputCheckboxGroup - dynamic: false - - name: playwright.controls.InputDarkMode - dynamic: false - - name: playwright.controls.InputDate - dynamic: false - - name: playwright.controls.InputDateRange - dynamic: false - - name: playwright.controls.InputFile - dynamic: false - - name: playwright.controls.InputNumeric - dynamic: false - - name: playwright.controls.InputPassword - dynamic: false - - name: playwright.controls.InputRadioButtons - dynamic: false - - name: playwright.controls.InputSelect - dynamic: false - - name: playwright.controls.InputSelectize - dynamic: false - - name: playwright.controls.InputSlider - dynamic: false - - name: playwright.controls.InputSliderRange - dynamic: false - - name: playwright.controls.InputSwitch - dynamic: false - - name: playwright.controls.InputTaskButton - dynamic: false - - name: playwright.controls.InputText - dynamic: false - - name: playwright.controls.InputTextArea - - title: Value boxes - desc: Methods for interacting with Shiny app valuebox controls. - contents: - - name: playwright.controls.ValueBox - dynamic: false - - title: Navigation (tab) panels - desc: Methods for interacting with Shiny app UI content controls. - contents: - - name: playwright.controls.NavItem - dynamic: false - - name: playwright.controls.NavsetBar - dynamic: false - - name: playwright.controls.NavsetCardPill - dynamic: false - - name: playwright.controls.NavsetCardTab - dynamic: false - - name: playwright.controls.NavsetCardUnderline - dynamic: false - - name: playwright.controls.NavsetHidden - dynamic: false - - name: playwright.controls.NavsetPill - dynamic: false - - name: playwright.controls.NavsetPillList - dynamic: false - - name: playwright.controls.NavsetTab - dynamic: false - - name: playwright.controls.NavsetUnderline - dynamic: false - - title: Upload and download - desc: Create UI for uploading and downloading files. - contents: - - name: playwright.controls.DownloadButton - dynamic: false - - name: playwright.controls.DownloadLink - dynamic: false - - title: Rendering outputs - desc: Render output in a variety of ways. - contents: - - name: playwright.controls.OutputCode - dynamic: false - - name: playwright.controls.OutputDataFrame - dynamic: false - - name: playwright.controls.OutputImage - dynamic: false - - name: playwright.controls.OutputPlot - dynamic: false - - name: playwright.controls.OutputTable - dynamic: false - - name: playwright.controls.OutputText - dynamic: false - - name: playwright.controls.OutputTextVerbatim - dynamic: false - - name: playwright.controls.OutputUi - - kind: page - path: PlaywrightExpect - flatten: true - summary: - name: "Playwright Expect" - desc: "Methods for testing the state of a locator within a Shiny app." - contents: - - name: playwright.expect.expect_to_change - dynamic: false - - name: playwright.expect.expect_attribute_to_have_value - dynamic: false - - name: playwright.expect.expect_to_have_class - dynamic: false - - name: playwright.expect.expect_not_to_have_class - dynamic: false - - name: playwright.expect.expect_to_have_style - dynamic: false - - kind: page - path: Pytest - flatten: true - summary: - name: "Pytest" - desc: "Fixtures used for testing Shiny apps with Pytest." - contents: - - name: pytest.create_app_fixture - dynamic: false - - name: pytest.ScopeName - dynamic: false - - kind: page - path: Run - flatten: true - summary: - name: "Run" - desc: "Methods for starting a local Shiny app in the background" - contents: - - name: run.run_shiny_app - dynamic: false - - name: run.ShinyAppProc - dynamic: false + - 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 diff --git a/shiny/_main.py b/shiny/_main.py index c23a32766..b57a686d4 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -521,6 +521,7 @@ def try_import_module(module: str) -> Optional[types.ModuleType]: type=str, help="Please provide the path to the app for which you want to create a test file.", ) +# Param for app.py, param for test_name def add_tests( app_dir: Path, ) -> None: From f897141ba489d20d0cc628c1e05e362a7856e892 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 21 Jun 2024 09:53:58 -0700 Subject: [PATCH 09/13] some code review suggestions --- docs/Makefile | 2 +- docs/_combine_objects_json.py | 2 +- docs/_quartodoc-core.yml | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 5823bcd06..8493a0e57 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -81,7 +81,7 @@ quartodoc_build_express: $(PYBIN) quartodoc_interlinks ## Build test API docs quartodoc_build_test: $(PYBIN) quartodoc_interlinks . $(PYBIN)/activate \ - && echo "::group::quartodoc build test docs" \ + && echo "::group::quartodoc build testing docs" \ && quartodoc build --config _quartodoc-testing.yml --verbose \ && mv objects.json _objects_express.json \ && echo "::endgroup::" diff --git a/docs/_combine_objects_json.py b/docs/_combine_objects_json.py index f16636409..b5080ecce 100644 --- a/docs/_combine_objects_json.py +++ b/docs/_combine_objects_json.py @@ -59,7 +59,7 @@ def write_objects_file(objects: QuartodocObject, path: str) -> None: print("Core:", objects_core.count) print("Express:", objects_express.count) -print("Test:", objects_test.count) +print("Testing:", objects_test.count) print("Combined:", objects_ret.count) # Save combined objects file info diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index f71960bba..a9c7c12f3 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -331,7 +331,6 @@ quartodoc: - types.SilentException - types.SilentCancelOutputException - types.SafeException - - title: Deprecated desc: "" contents: From 397a54106c8cd5403d874844285f5ec3365e8e7f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 21 Jun 2024 16:55:25 -0400 Subject: [PATCH 10/13] Add nested `click` support. Verify app file exists. Verify test file does not exist. --- shiny/_main.py | 24 ++++++--- shiny/_template_utils.py | 111 +++++++++++++++++++++++++++++++++++---- 2 files changed, 118 insertions(+), 17 deletions(-) diff --git a/shiny/_main.py b/shiny/_main.py index b57a686d4..d6e08b208 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -503,7 +503,12 @@ def try_import_module(module: str) -> Optional[types.ModuleType]: } -@main.command( +@main.group(help="""Add files to enhance your app.""") +def add() -> None: + pass + + +@add.command( help="""Add a test file for a specified app. Add an empty test file for a specified app. You will be prompted with @@ -512,22 +517,29 @@ def try_import_module(module: str) -> Optional[types.ModuleType]: After creating the shiny app file, you can use `pytest` to run the tests: - pytest APPDIR/test_APPNAME.py + pytest TEST_FILE """ ) @click.option( "--app", "-a", type=str, - help="Please provide the path to the app for which you want to create a test file.", + 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 uniquely named test file (independent of folder structure).", ) # Param for app.py, param for test_name -def add_tests( - app_dir: Path, +def test( + app: Path | None, + test_file: Path | None, ) -> None: from ._template_utils import add_test_file - add_test_file(app_dir) + add_test_file(app_file=app, test_file=test_file) @main.command( diff --git a/shiny/_template_utils.py b/shiny/_template_utils.py index a3cfd1361..578934b74 100644 --- a/shiny/_template_utils.py +++ b/shiny/_template_utils.py @@ -324,20 +324,109 @@ def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = app_dir) return app_dir -def add_test_file(app_dir: Path): - test_file = app_dir / "test_app.py" - test_file.write_text( -""" -from shiny.playwright.controls import -from shiny.run import ShinyAppProc -from playwright.sync_api import Page -from shiny.pytest import create_app_fixture +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 + 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." + return ( + not Path(x).exists() + ) or "Test file already exists. Please provide a new file name." + + 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 + + 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 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 + + # Make sure test file directory exists + test_file.parent.mkdir(parents=True, exist_ok=True) -app = create_app_fixture(f"{app_dir}/app.py") + # Write template to test file + header = """\ +from playwright.sync_api import Page +from shiny.playwright.controls import +""" + if not is_same_dir: + header = ( + header + + """from shiny.pytest import create_app_fixture +""" + ) + header = ( + header + + """\ +from shiny.run import ShinyAppProc""" + ) -def test_app(page: Page, app: ShinyAppProc): + test_body = """\ page.goto(app.url) # Add tests code here """ - ) + + test_name = test_file.name.replace(".py", "") + + if is_same_dir: + middle = f"""\ + +def {test_name}(page: Page, local_app: ShinyAppProc): +""" + else: + rel_path = os.path.relpath(app_file, test_file.parent) + middle = f"""\ + +app = create_app_fixture("{rel_path}") + + +def {test_name}(page: Page, app: ShinyAppProc): +""" + + test_file.write_text("\n".join([header, middle, test_body])) From 99f203334d366175e0932d58c4185328977e2aed Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 21 Jun 2024 16:59:55 -0400 Subject: [PATCH 11/13] Fix quartdoc bug --- docs/.gitignore | 3 +-- docs/Makefile | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/.gitignore b/docs/.gitignore index b65bacee6..f2af3e74d 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -5,5 +5,4 @@ _sidebar.yml /.quarto/ objects.json site_libs/ -_objects_core.json -_objects_express.json +_objects_*.json diff --git a/docs/Makefile b/docs/Makefile index 8493a0e57..ce5c6c74b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -83,7 +83,7 @@ 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_express.json \ + && mv objects.json _objects_test.json \ && echo "::endgroup::" ## Clean up after quartodoc build From fbc19160ed796dd637d8066d03312332f0562374 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 21 Jun 2024 17:12:16 -0400 Subject: [PATCH 12/13] Simpler templating. --- shiny/_template_utils.py | 64 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/shiny/_template_utils.py b/shiny/_template_utils.py index 578934b74..26e97d412 100644 --- a/shiny/_template_utils.py +++ b/shiny/_template_utils.py @@ -346,6 +346,7 @@ def path_exists(x: Path) -> bool | str: ).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) @@ -357,9 +358,11 @@ def path_does_not_exist(x: Path) -> bool | str: return False if Path(x).is_dir(): return "Please provide a file path for your test file." - return ( - not Path(x).exists() - ) or "Test file already exists. Please provide a new file name." + 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:", @@ -371,6 +374,7 @@ def path_does_not_exist(x: Path) -> bool | str: else: test_file_val = test_file + # User quit early if test_file_val is None: sys.exit(1) test_file = Path(test_file_val) @@ -381,52 +385,48 @@ def path_does_not_exist(x: Path) -> bool | str: # 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 - # Make sure test file directory exists - test_file.parent.mkdir(parents=True, exist_ok=True) + test_name = test_file.name.replace(".py", "") + rel_path = os.path.relpath(app_file, test_file.parent) - # Write template to test file - header = """\ + template = ( + f"""\ from playwright.sync_api import Page from shiny.playwright.controls import -""" - if not is_same_dir: - header = ( - header - + """from shiny.pytest import create_app_fixture -""" - ) - header = ( - header - + """\ -from shiny.run import ShinyAppProc""" - ) - - test_body = """\ - page.goto(app.url) - # Add tests code here -""" - - test_name = test_file.name.replace(".py", "") +from shiny.run import ShinyAppProc - if is_same_dir: - middle = f"""\ def {test_name}(page: Page, local_app: ShinyAppProc): + + page.goto(local_app.url) + # Add tests code here """ - else: - rel_path = os.path.relpath(app_file, test_file.parent) - middle = f"""\ + if is_same_dir + else f"""\ +from playwright.sync_api import Page + +from shiny.playwright.controls import +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) - test_file.write_text("\n".join([header, middle, test_body])) + # Write template to test file + test_file.write_text(template) From 018d7f4cffb60596d00b03a43948971dd4990319 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 21 Jun 2024 17:12:23 -0400 Subject: [PATCH 13/13] Code review --- docs/_quartodoc-testing.yml | 9 --------- shiny/_main.py | 12 ++++++------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/docs/_quartodoc-testing.yml b/docs/_quartodoc-testing.yml index 400c3e1dc..e9c7de237 100644 --- a/docs/_quartodoc-testing.yml +++ b/docs/_quartodoc-testing.yml @@ -10,15 +10,6 @@ quartodoc: style: _renderer.py show_signature_annotations: false sections: - # - title: Testing - # desc: "Testing Methods to make it easier to run and test user's Shiny apps." - # contents: - # - kind: page - # path: PlaywrightControls - # flatten: true - # summary: - # name: "Playwright Controls" - # desc: "Methods for interacting with Shiny app controls." - title: UI Layouts desc: Methods for interacting with Shiny app multiple UI component controls. contents: diff --git a/shiny/_main.py b/shiny/_main.py index d6e08b208..1581362df 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -503,17 +503,17 @@ def try_import_module(module: str) -> Optional[types.ModuleType]: } -@main.group(help="""Add files to enhance your app.""") +@main.group(help="""Add files to enhance your Shiny app.""") def add() -> None: pass @add.command( - help="""Add a test file for a specified app. + 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. +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: @@ -530,7 +530,7 @@ def add() -> None: "--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 uniquely named test file (independent of folder structure).", + 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(