From 7f9d2360341891dad07c0dd229aff8aaf213f3bf Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Wed, 22 May 2024 05:25:26 -0700 Subject: [PATCH 01/14] allow conftest functions to be imported as shiny.test module --- shiny/test/__init__.py | 24 +++ .../conftest.py => shiny/test/_conftest.py | 152 +----------------- shiny/test/_expect.py | 92 +++++++++++ shiny/test/_internal.py | 61 +++++++ 4 files changed, 178 insertions(+), 151 deletions(-) create mode 100644 shiny/test/__init__.py rename tests/playwright/conftest.py => shiny/test/_conftest.py (64%) create mode 100644 shiny/test/_expect.py create mode 100644 shiny/test/_internal.py diff --git a/shiny/test/__init__.py b/shiny/test/__init__.py new file mode 100644 index 000000000..a674f7bf0 --- /dev/null +++ b/shiny/test/__init__.py @@ -0,0 +1,24 @@ +try: + import pytest_playwright # noqa: F401 # pyright: ignore[reportUnusedImport, reportMissingTypeStubs] +except ImportError: + raise ImportError( + "The shiny.test module requires the pytest-playwright package to be installed." + " Please install it with this command:" + "\n\n pip install pytest-playwright" + ) +from playwright.sync_api import Page, expect + + +from ._conftest import ShinyAppProc, create_app_fixture, local_app, run_shiny_app +from ._expect import expect_to_change, retry_with_timeout + +__all__ = ( + "Page", + "expect", + "ShinyAppProc", + "create_app_fixture", + "local_app", + "run_shiny_app", + "expect_to_change", + "retry_with_timeout", +) diff --git a/tests/playwright/conftest.py b/shiny/test/_conftest.py similarity index 64% rename from tests/playwright/conftest.py rename to shiny/test/_conftest.py index 6c38ac0dc..5a8324b55 100644 --- a/tests/playwright/conftest.py +++ b/shiny/test/_conftest.py @@ -1,27 +1,13 @@ from __future__ import annotations import datetime -import functools import logging import subprocess import sys import threading -import time -from contextlib import contextmanager from pathlib import PurePath from types import TracebackType -from typing import ( - IO, - Any, - Callable, - Generator, - List, - Literal, - Optional, - TextIO, - Type, - Union, -) +from typing import IO, Callable, Generator, List, Literal, Optional, TextIO, Type, Union import pytest @@ -30,12 +16,8 @@ __all__ = ( "ShinyAppProc", "create_app_fixture", - "create_doc_example_core_fixture", - "create_example_fixture", "local_app", "run_shiny_app", - "expect_to_change", - "retry_with_timeout", ) from playwright.sync_api import BrowserContext, Page @@ -59,10 +41,6 @@ def page(session_page: Page) -> Page: return session_page -here = PurePath(__file__).parent -here_root = here.parent.parent - - class OutputStream: """Designed to wrap an IO[str] and accumulate the output using a bg thread @@ -264,135 +242,7 @@ def fixture_func(): return fixture_func -def create_example_fixture( - example_name: str, - example_file: str = "app.py", - scope: ScopeName = "module", -): - """Used to create app fixtures from apps in py-shiny/examples""" - return create_app_fixture( - here_root / "examples" / example_name / example_file, scope - ) - - -def create_doc_example_fixture( - example_name: str, - example_file: str = "app.py", - scope: ScopeName = "module", -): - """Used to create app fixtures from apps in py-shiny/shiny/api-examples""" - return create_app_fixture( - here_root / "shiny/api-examples" / example_name / example_file, scope - ) - - -def create_doc_example_core_fixture( - example_name: str, - scope: ScopeName = "module", -): - """Used to create app fixtures from ``app-core.py`` example apps in py-shiny/shiny/api-examples""" - return create_doc_example_fixture(example_name, "app-core.py", scope) - - -def create_doc_example_express_fixture( - example_name: str, - scope: ScopeName = "module", -): - """Used to create app fixtures from ``app-express.py`` example apps in py-shiny/shiny/api-examples""" - return create_doc_example_fixture(example_name, "app-express.py", scope) - - -def x_create_doc_example_fixture(example_name: str, scope: ScopeName = "module"): - """Used to create app fixtures from apps in py-shiny/shiny/examples""" - return create_app_fixture( - here_root / "shiny/experimental/api-examples" / example_name / "app.py", scope - ) - - @pytest.fixture(scope="module") def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, None]: app_gen = local_app_fixture_gen(PurePath(request.path).parent / "app.py") yield next(app_gen) - - -@contextmanager -def expect_to_change( - func: Callable[[], Any], timeoutSecs: float = 10 -) -> Generator[None, None, None]: - """ - Context manager that yields when the value returned by func() changes. Use this - around code that has a side-effect of changing some state asynchronously (such as - all browser actions), to prevent moving onto the next step of the test until this - one has actually taken effect. - - Raises TimeoutError if the value does not change within timeoutSecs. - - Parameters - ---------- - func - A function that returns a value. The value returned by this function is - compared to the value returned by subsequent calls to this function. - timeoutSecs - How long to wait for the value to change before raising TimeoutError. - - Example - ------- - - with expect_to_change(lambda: page.locator("#name").value()): - page.keyboard.send_keys("hello") - - """ - - original_value = func() - yield - - @retry_with_timeout(timeoutSecs) - def wait_for_change(): - if func() == original_value: - raise AssertionError("Value did not change") - - wait_for_change() - - -def retry_with_timeout(timeout: float = 30): - """ - Decorator that retries a function until 1) it succeeds, 2) fails with a - non-assertion error, or 3) repeatedly fails with an AssertionError for longer than - the timeout. If the timeout elapses, the last AssertionError is raised. - - Parameters - ---------- - timeout - How long to wait for the function to succeed before raising the last - AssertionError. - - Returns - ------- - A decorator that can be applied to a function. - - Example - ------- - - @retry_with_timeout(30) - def try_to_find_element(): - if not page.locator("#name").exists(): - raise AssertionError("Element not found") - - try_to_find_element() - """ - - def decorator(func: Callable[[], None]) -> Callable[[], None]: - @functools.wraps(func) - def wrapper() -> None: - start = time.time() - while True: - try: - return func() - except AssertionError as e: - if time.time() - start > timeout: - raise e - time.sleep(0.1) - - return wrapper - - return decorator diff --git a/shiny/test/_expect.py b/shiny/test/_expect.py new file mode 100644 index 000000000..d080f4c5d --- /dev/null +++ b/shiny/test/_expect.py @@ -0,0 +1,92 @@ +import functools +import time +from contextlib import contextmanager +from typing import Any, Callable, Generator + +__all__ = ( + "expect_to_change", + "retry_with_timeout", +) + + +@contextmanager +def expect_to_change( + func: Callable[[], Any], timeoutSecs: float = 10 +) -> Generator[None, None, None]: + """ + Context manager that yields when the value returned by func() changes. Use this + around code that has a side-effect of changing some state asynchronously (such as + all browser actions), to prevent moving onto the next step of the test until this + one has actually taken effect. + + Raises TimeoutError if the value does not change within timeoutSecs. + + Parameters + ---------- + func + A function that returns a value. The value returned by this function is + compared to the value returned by subsequent calls to this function. + timeoutSecs + How long to wait for the value to change before raising TimeoutError. + + Example + ------- + + with expect_to_change(lambda: page.locator("#name").value()): + page.keyboard.send_keys("hello") + + """ + + original_value = func() + yield + + @retry_with_timeout(timeoutSecs) + def wait_for_change(): + if func() == original_value: + raise AssertionError("Value did not change") + + wait_for_change() + + +def retry_with_timeout(timeout: float = 30): + """ + Decorator that retries a function until 1) it succeeds, 2) fails with a + non-assertion error, or 3) repeatedly fails with an AssertionError for longer than + the timeout. If the timeout elapses, the last AssertionError is raised. + + Parameters + ---------- + timeout + How long to wait for the function to succeed before raising the last + AssertionError. + + Returns + ------- + A decorator that can be applied to a function. + + Example + ------- + + @retry_with_timeout(30) + def try_to_find_element(): + if not page.locator("#name").exists(): + raise AssertionError("Element not found") + + try_to_find_element() + """ + + def decorator(func: Callable[[], None]) -> Callable[[], None]: + @functools.wraps(func) + def wrapper() -> None: + start = time.time() + while True: + try: + return func() + except AssertionError as e: + if time.time() - start > timeout: + raise e + time.sleep(0.1) + + return wrapper + + return decorator diff --git a/shiny/test/_internal.py b/shiny/test/_internal.py new file mode 100644 index 000000000..92c93e6fc --- /dev/null +++ b/shiny/test/_internal.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from pathlib import PurePath + +from ._conftest import ScopeName, create_app_fixture + +__all__ = ( + "create_doc_example_fixture", + "create_example_fixture", + "create_doc_example_core_fixture", + "create_doc_example_express_fixture", +) + + +here = PurePath(__file__).parent +here_root = here.parent.parent + + +def create_example_fixture( + example_name: str, + example_file: str = "app.py", + scope: ScopeName = "module", +): + """Used to create app fixtures from apps in py-shiny/examples""" + return create_app_fixture( + here_root / "examples" / example_name / example_file, scope + ) + + +def create_doc_example_fixture( + example_name: str, + example_file: str = "app.py", + scope: ScopeName = "module", +): + """Used to create app fixtures from apps in py-shiny/shiny/api-examples""" + return create_app_fixture( + here_root / "shiny/api-examples" / example_name / example_file, scope + ) + + +def create_doc_example_core_fixture( + example_name: str, + scope: ScopeName = "module", +): + """Used to create app fixtures from ``app-core.py`` example apps in py-shiny/shiny/api-examples""" + return create_doc_example_fixture(example_name, "app-core.py", scope) + + +def create_doc_example_express_fixture( + example_name: str, + scope: ScopeName = "module", +): + """Used to create app fixtures from ``app-express.py`` example apps in py-shiny/shiny/api-examples""" + return create_doc_example_fixture(example_name, "app-express.py", scope) + + +# def x_create_doc_example_fixture(example_name: str, scope: ScopeName = "module"): +# """Used to create app fixtures from apps in py-shiny/shiny/examples""" +# return create_app_fixture( +# here_root / "shiny/experimental/api-examples" / example_name / "app.py", scope +# ) From 52ab838be3f9bc7fd9b813f79f7db7c44d766e78 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 24 May 2024 09:54:08 -0700 Subject: [PATCH 02/14] refactor tests to use shiny.test --- setup.cfg | 3 + shiny/test/__init__.py | 20 ++--- shiny/test/_conftest.py | 80 +------------------ .../controls.py => shiny/test/_controls.py | 13 ++- shiny/test/_expect.py | 5 +- shiny/test/_pytest.py | 34 ++++++++ shiny/test/fixture/__init__.py | 7 ++ shiny/test/fixture/_fixture.py | 63 +++++++++++++++ shiny/test/playwright.py | 17 ++++ tests/playwright/README.md | 8 +- .../playwright/conftest.py | 12 +-- .../test_deploys_express_accordion.py | 5 +- .../test_deploys_express_dataframe.py | 5 +- .../test_deploys_express_folium.py | 3 +- .../test_deploys_express_page_default.py | 5 +- .../test_deploys_express_page_fillable.py | 5 +- .../test_deploys_express_page_fluid.py | 5 +- .../test_deploys_express_page_sidebar.py | 5 +- .../deploys/plotly/test_plotly_app.py | 3 +- .../test_shiny_client_error.py | 3 +- tests/playwright/examples/example_apps.py | 3 +- .../playwright/examples/test_api_examples.py | 3 +- tests/playwright/examples/test_examples.py | 3 +- .../playwright/examples/test_shiny_create.py | 2 +- .../_internal/test_e2e_regex_matching.py | 2 +- tests/playwright/shiny/async/test_async.py | 5 +- .../test_update_slider_datetime_value.py | 5 +- .../bugs/0666-sidebar/test_sidebar_colors.py | 6 +- .../test_0676_row_selection.py | 3 +- .../shiny/bugs/0696-resolve-id/mod_state.py | 4 +- .../0696-resolve-id/test_0696_resolve_id.py | 13 ++- .../test_1345_input_selected_rows.py | 5 +- .../test_1351_selected_row.py | 5 +- .../components/accordion/test_accordion.py | 6 +- .../busy_indicators/test_busy_indicators.py | 5 +- .../components/card-input/test_card-input.py | 7 +- .../data_frame/example/test_data_frame.py | 8 +- .../df_organization/test_df_organization.py | 5 +- .../data_frame/html_columns_df/tabbing/app.py | 4 +- .../html_columns_df/tabbing/test_tabbing.py | 5 +- .../html_columns_df/test_html_columns.py | 5 +- .../row_selection/test_row_selection.py | 5 +- .../test_validate_data_edit_mode.py | 5 +- .../test_validate_data_save.py | 5 +- .../layout_columns/test_layout_columns.py | 5 +- .../shiny/components/nav/test_nav.py | 6 +- .../navset_hidden/test_nav_hidden.py | 5 +- .../shiny/components/popover/test_popover.py | 5 +- .../shiny/components/test_sidebar.py | 7 +- .../shiny/components/tooltip/test_tooltip.py | 5 +- .../value_box/kitchensink/test_valuebox_ks.py | 5 +- .../value_box/smoke/test_valuebox.py | 6 +- .../test_default_render_ui.py | 3 +- .../test_output_transformer_example.py | 5 +- .../card/kitchensink/test_card_ks.py | 5 +- .../shiny/experimental/card/test_card.py | 5 +- .../test_implicit_register.py | 3 +- .../playwright/shiny/inputs/input_file/app.py | 4 +- .../inputs/input_file/test_input_file.py | 6 +- .../test_input_radio_checkbox_group_app.py | 5 +- .../input_slider/test_input_slider_app.py | 6 +- .../test_input_task_button.py | 5 +- .../test_input_task_button2.py | 5 +- .../inputs/test_input_action_button_link.py | 7 +- .../shiny/inputs/test_input_checkbox.py | 7 +- .../shiny/inputs/test_input_checkbox_group.py | 7 +- .../shiny/inputs/test_input_dark_mode.py | 7 +- .../shiny/inputs/test_input_date.py | 7 +- .../shiny/inputs/test_input_date_range.py | 7 +- .../shiny/inputs/test_input_numeric.py | 7 +- .../shiny/inputs/test_input_password.py | 7 +- .../shiny/inputs/test_input_radio_buttons.py | 7 +- .../shiny/inputs/test_input_select.py | 7 +- .../shiny/inputs/test_input_selectize.py | 7 +- .../shiny/inputs/test_input_slider.py | 7 +- .../shiny/inputs/test_input_switch.py | 7 +- .../shiny/inputs/test_input_text.py | 7 +- .../shiny/inputs/test_input_text_area.py | 7 +- .../shiny/inputs/test_inputs_update.py | 7 +- .../test_module_conditional.py | 5 +- .../shiny/outputs/test_output_image.py | 7 +- .../shiny/outputs/test_output_plot.py | 7 +- .../shiny/outputs/test_output_table.py | 7 +- .../shiny/outputs/test_output_text.py | 7 +- .../shiny/outputs/test_output_ui.py | 7 +- .../shiny/plot-sizing/test_plot_sizing.py | 5 +- .../test_output_transformer_async.py | 5 +- .../reactive_event/test_reactive_event.py | 5 +- .../shiny/session/flush/test_on_flush.py | 5 +- .../shiny/shiny-express/hold/test_hold.py | 10 +-- .../render_express/test_render_express.py | 5 +- tests/playwright/utils/deploy_utils.py | 3 +- 92 files changed, 374 insertions(+), 332 deletions(-) rename tests/playwright/controls.py => shiny/test/_controls.py (99%) create mode 100644 shiny/test/_pytest.py create mode 100644 shiny/test/fixture/__init__.py create mode 100644 shiny/test/fixture/_fixture.py create mode 100644 shiny/test/playwright.py rename shiny/test/_internal.py => tests/playwright/conftest.py (80%) diff --git a/setup.cfg b/setup.cfg index d63ae762a..2ed73f94f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -124,9 +124,12 @@ include = shiny, shiny.* shiny = py.typed [options.entry_points] +pytest11 = + shiny-test = shiny.test._pytest console_scripts = shiny = shiny._main:main + [flake8] # E302: Expected 2 blank lines # E501: Line too long diff --git a/shiny/test/__init__.py b/shiny/test/__init__.py index a674f7bf0..2b18a6b96 100644 --- a/shiny/test/__init__.py +++ b/shiny/test/__init__.py @@ -6,19 +6,21 @@ " Please install it with this command:" "\n\n pip install pytest-playwright" ) -from playwright.sync_api import Page, expect -from ._conftest import ShinyAppProc, create_app_fixture, local_app, run_shiny_app -from ._expect import expect_to_change, retry_with_timeout +from ._conftest import ( + ShinyAppProc, + # run_shiny_app, +) + +# from ._expect import expect_to_change +from .playwright import Page, expect, Locator __all__ = ( - "Page", "expect", + # "run_shiny_app", + # "expect_to_change", "ShinyAppProc", - "create_app_fixture", - "local_app", - "run_shiny_app", - "expect_to_change", - "retry_with_timeout", + "Page", + "Locator", ) diff --git a/shiny/test/_conftest.py b/shiny/test/_conftest.py index 5a8324b55..9d425be81 100644 --- a/shiny/test/_conftest.py +++ b/shiny/test/_conftest.py @@ -1,45 +1,20 @@ from __future__ import annotations import datetime -import logging import subprocess import sys import threading from pathlib import PurePath from types import TracebackType -from typing import IO, Callable, Generator, List, Literal, Optional, TextIO, Type, Union - -import pytest +from typing import IO, Callable, List, Optional, TextIO, Type, Union import shiny._utils __all__ = ( "ShinyAppProc", - "create_app_fixture", - "local_app", "run_shiny_app", ) -from playwright.sync_api import BrowserContext, Page - - -# Make a single page fixture that can be used by all tests -@pytest.fixture(scope="session") -# By using a single page, the browser is only launched once and all tests run in the same tab / page. -def session_page(browser: BrowserContext) -> Page: - return browser.new_page() - - -@pytest.fixture(scope="function") -# By going to `about:blank`, we _reset_ the page to a known state before each test. -# It is not perfect, but it is faster than making a new page for each test. -# This must be done before each test -def page(session_page: Page) -> Page: - session_page.goto("about:blank") - # Reset screen size to 1080p - session_page.set_viewport_size({"width": 1920, "height": 1080}) - return session_page - class OutputStream: """Designed to wrap an IO[str] and accumulate the output using a bg thread @@ -193,56 +168,3 @@ def run_shiny_app( if wait_for_start: sa.wait_until_ready(timeout_secs) return sa - - -# Attempt up to 3 times to start the app, with a random port each time -def local_app_fixture_gen(app: PurePath | str): - - has_yielded_app = False - remaining_attempts = 3 - while not has_yielded_app and remaining_attempts > 0: - remaining_attempts -= 1 - - # Make shiny process - sa = run_shiny_app(app, wait_for_start=False, port=0) - try: - # enter / exit shiny context manager; (closes streams on exit) - with sa: - # Wait for shiny app to start - # Could throw a `ConnectionError` if the port is already in use - sa.wait_until_ready(30) - # Run app! - has_yielded_app = True - yield sa - except ConnectionError as e: - if remaining_attempts == 0: - # Ran out of attempts! - raise e - print(f"Failed to bind to port: {e}", file=sys.stderr) - # Try again with a new port! - finally: - if has_yielded_app: - logging.warning("Application output:\n" + str(sa.stderr)) - - -ScopeName = Literal["session", "package", "module", "class", "function"] - - -def create_app_fixture( - app: Union[PurePath, str], - scope: ScopeName = "module", -): - @pytest.fixture(scope=scope) - def fixture_func(): - # Pass through `yield` via `next(...)` call - # (`yield` must be on same line as `next`!) - app_gen = local_app_fixture_gen(app) - yield next(app_gen) - - return fixture_func - - -@pytest.fixture(scope="module") -def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, None]: - app_gen = local_app_fixture_gen(PurePath(request.path).parent / "app.py") - yield next(app_gen) diff --git a/tests/playwright/controls.py b/shiny/test/_controls.py similarity index 99% rename from tests/playwright/controls.py rename to shiny/test/_controls.py index c78cfb452..6d81b4e9e 100644 --- a/tests/playwright/controls.py +++ b/shiny/test/_controls.py @@ -11,18 +11,15 @@ import typing from typing import Literal, Optional, Protocol -from playwright.sync_api import FilePayload, FloatRect, Locator, Page, Position -from playwright.sync_api import expect as playwright_expect - # Import `shiny`'s typing extentions. # Since this is a private file, tell pyright to ignore the import -from shiny._typing_extensions import ( - TypeGuard, # pyright: ignore[reportPrivateImportUsage] -) -from shiny._typing_extensions import ( +from .._typing_extensions import TypeGuard # pyright: ignore[reportPrivateImportUsage] +from .._typing_extensions import ( assert_type, # pyright: ignore[reportPrivateImportUsage] ) -from shiny.types import MISSING, MISSING_TYPE +from ..types import MISSING, MISSING_TYPE +from .playwright import FilePayload, FloatRect, Locator, Page, Position +from .playwright import expect as playwright_expect """ Questions: diff --git a/shiny/test/_expect.py b/shiny/test/_expect.py index d080f4c5d..98e5d178c 100644 --- a/shiny/test/_expect.py +++ b/shiny/test/_expect.py @@ -3,10 +3,7 @@ from contextlib import contextmanager from typing import Any, Callable, Generator -__all__ = ( - "expect_to_change", - "retry_with_timeout", -) +__all__ = ("expect_to_change",) @contextmanager diff --git a/shiny/test/_pytest.py b/shiny/test/_pytest.py new file mode 100644 index 000000000..7a6643e76 --- /dev/null +++ b/shiny/test/_pytest.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import PurePath +from typing import Generator + +import pytest + +from ._conftest import ShinyAppProc +from .fixture import local_app_fixture_gen +from .playwright import BrowserContext, Page + + +# Make a single page fixture that can be used by all tests +@pytest.fixture(scope="session") +# By using a single page, the browser is only launched once and all tests run in the same tab / page. +def session_page(browser: BrowserContext) -> Page: + return browser.new_page() + + +@pytest.fixture(scope="function") +# By going to `about:blank`, we _reset_ the page to a known state before each test. +# It is not perfect, but it is faster than making a new page for each test. +# This must be done before each test +def page(session_page: Page) -> Page: + session_page.goto("about:blank") + # Reset screen size to 1080p + session_page.set_viewport_size({"width": 1920, "height": 1080}) + return session_page + + +@pytest.fixture(scope="module") +def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, None]: + app_gen = local_app_fixture_gen(PurePath(request.path).parent / "app.py") + yield next(app_gen) diff --git a/shiny/test/fixture/__init__.py b/shiny/test/fixture/__init__.py new file mode 100644 index 000000000..729cf084e --- /dev/null +++ b/shiny/test/fixture/__init__.py @@ -0,0 +1,7 @@ +from ._fixture import create_app_fixture, local_app_fixture_gen, ScopeName + +__all__ = ( + "create_app_fixture", + "local_app_fixture_gen", + "ScopeName", +) diff --git a/shiny/test/fixture/_fixture.py b/shiny/test/fixture/_fixture.py new file mode 100644 index 000000000..5fa357059 --- /dev/null +++ b/shiny/test/fixture/_fixture.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import logging +import sys +from pathlib import PurePath +from typing import Literal, Union + +import pytest + +from .._conftest import run_shiny_app + +__all__ = ( + "create_app_fixture", + "local_app_fixture_gen", + "ScopeName", +) + + +# Attempt up to 3 times to start the app, with a random port each time +def local_app_fixture_gen(app: PurePath | str): + + has_yielded_app = False + remaining_attempts = 3 + while not has_yielded_app and remaining_attempts > 0: + remaining_attempts -= 1 + + # Make shiny process + sa = run_shiny_app(app, wait_for_start=False, port=0) + try: + # enter / exit shiny context manager; (closes streams on exit) + with sa: + # Wait for shiny app to start + # Could throw a `ConnectionError` if the port is already in use + sa.wait_until_ready(30) + # Run app! + has_yielded_app = True + yield sa + except ConnectionError as e: + if remaining_attempts == 0: + # Ran out of attempts! + raise e + print(f"Failed to bind to port: {e}", file=sys.stderr) + # Try again with a new port! + finally: + if has_yielded_app: + logging.warning("Application output:\n" + str(sa.stderr)) + + +ScopeName = Literal["session", "package", "module", "class", "function"] + + +def create_app_fixture( + app: Union[PurePath, str], + scope: ScopeName = "module", +): + @pytest.fixture(scope=scope) + def fixture_func(): + # Pass through `yield` via `next(...)` call + # (`yield` must be on same line as `next`!) + app_gen = local_app_fixture_gen(app) + yield next(app_gen) + + return fixture_func diff --git a/shiny/test/playwright.py b/shiny/test/playwright.py new file mode 100644 index 000000000..06a561f70 --- /dev/null +++ b/shiny/test/playwright.py @@ -0,0 +1,17 @@ +from playwright.sync_api import BrowserContext as BrowserContext +from playwright.sync_api import FilePayload as FilePayload +from playwright.sync_api import FloatRect as FloatRect +from playwright.sync_api import Locator as Locator +from playwright.sync_api import Page as Page +from playwright.sync_api import Position as Position +from playwright.sync_api import expect as expect + +__all__ = ( + "expect", + "Page", + "FilePayload", + "Locator", + "BrowserContext", + "FloatRect", + "Position", +) diff --git a/tests/playwright/README.md b/tests/playwright/README.md index 164857b94..5f4d3fa52 100644 --- a/tests/playwright/README.md +++ b/tests/playwright/README.md @@ -49,8 +49,7 @@ as the calling `test_*.py` file. ```python import re -from playwright.sync_api import Page, expect -from conftest import ShinyAppProc +from shiny.test import Page, ShinyAppProc, expect def test_airmass(page: Page, local_app: ShinyAppProc): @@ -68,9 +67,8 @@ use it from test funcs. ```python import re -from playwright.sync_api import Page, expect - -from conftest import ShinyAppProc, create_example_fixture +from shiny.test import Page, ShinyAppProc +from shiny.test._internal import create_example_fixture airmass_app = create_example_fixture("airmass") diff --git a/shiny/test/_internal.py b/tests/playwright/conftest.py similarity index 80% rename from shiny/test/_internal.py rename to tests/playwright/conftest.py index 92c93e6fc..4dfc1897b 100644 --- a/shiny/test/_internal.py +++ b/tests/playwright/conftest.py @@ -1,8 +1,11 @@ +# This file is necessary for pytest to find relative module files +# such as examples/example_apps.py + from __future__ import annotations from pathlib import PurePath -from ._conftest import ScopeName, create_app_fixture +from shiny.test.fixture import ScopeName, create_app_fixture __all__ = ( "create_doc_example_fixture", @@ -52,10 +55,3 @@ def create_doc_example_express_fixture( ): """Used to create app fixtures from ``app-express.py`` example apps in py-shiny/shiny/api-examples""" return create_doc_example_fixture(example_name, "app-express.py", scope) - - -# def x_create_doc_example_fixture(example_name: str, scope: ScopeName = "module"): -# """Used to create app fixtures from apps in py-shiny/shiny/examples""" -# return create_app_fixture( -# here_root / "shiny/experimental/api-examples" / example_name / "app.py", scope -# ) diff --git a/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py b/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py index b734f3fdc..a8ac5c39e 100644 --- a/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py +++ b/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py @@ -1,6 +1,4 @@ import pytest -from controls import Accordion -from playwright.sync_api import Page from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -8,6 +6,9 @@ skip_if_not_chrome, ) +from shiny.test import Page +from shiny.test._controls import Accordion + app_url = create_deploys_app_url_fixture("shiny_express_accordion") diff --git a/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py b/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py index b08d62502..d9107aab7 100644 --- a/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py +++ b/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py @@ -1,6 +1,4 @@ import pytest -from controls import OutputDataFrame -from playwright.sync_api import Page from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -8,6 +6,9 @@ skip_if_not_chrome, ) +from shiny.test import Page +from shiny.test._controls import OutputDataFrame + app_url = create_deploys_app_url_fixture("shiny-express-dataframe") diff --git a/tests/playwright/deploys/express-folium/test_deploys_express_folium.py b/tests/playwright/deploys/express-folium/test_deploys_express_folium.py index 247fc0b89..49772c4f1 100644 --- a/tests/playwright/deploys/express-folium/test_deploys_express_folium.py +++ b/tests/playwright/deploys/express-folium/test_deploys_express_folium.py @@ -1,5 +1,4 @@ import pytest -from playwright.sync_api import Page, expect from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -7,6 +6,8 @@ skip_if_not_chrome, ) +from shiny.test import Page, expect + app_url = create_deploys_app_url_fixture("shiny-express-folium") diff --git a/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py b/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py index 264504bb6..b6bedb338 100644 --- a/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py +++ b/tests/playwright/deploys/express-page_default/test_deploys_express_page_default.py @@ -1,6 +1,4 @@ import pytest -from controls import LayoutNavsetTab -from playwright.sync_api import Page, expect from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -8,6 +6,9 @@ skip_if_not_chrome, ) +from shiny.test import Page, expect +from shiny.test._controls import LayoutNavsetTab + TIMEOUT = 2 * 60 * 1000 app_url = create_deploys_app_url_fixture("shiny_express_page_default") diff --git a/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py b/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py index 58c8809f9..36874787e 100644 --- a/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py +++ b/tests/playwright/deploys/express-page_fillable/test_deploys_express_page_fillable.py @@ -1,6 +1,4 @@ import pytest -from controls import Card, OutputTextVerbatim -from playwright.sync_api import Page from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -8,6 +6,9 @@ skip_if_not_chrome, ) +from shiny.test import Page +from shiny.test._controls import Card, OutputTextVerbatim + app_url = create_deploys_app_url_fixture("express_page_fillable") diff --git a/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py b/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py index 2e4c1efaa..a03157621 100644 --- a/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py +++ b/tests/playwright/deploys/express-page_fluid/test_deploys_express_page_fluid.py @@ -1,6 +1,4 @@ import pytest -from controls import Card, OutputTextVerbatim -from playwright.sync_api import Page from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -8,6 +6,9 @@ skip_if_not_chrome, ) +from shiny.test import Page +from shiny.test._controls import Card, OutputTextVerbatim + app_url = create_deploys_app_url_fixture("express_page_fluid") diff --git a/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py b/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py index 4ff92f2b5..10cddd5b0 100644 --- a/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py +++ b/tests/playwright/deploys/express-page_sidebar/test_deploys_express_page_sidebar.py @@ -1,6 +1,4 @@ import pytest -from controls import OutputTextVerbatim, Sidebar -from playwright.sync_api import Page from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -8,6 +6,9 @@ skip_if_not_chrome, ) +from shiny.test import Page +from shiny.test._controls import OutputTextVerbatim, Sidebar + app_url = create_deploys_app_url_fixture("express_page_sidebar") diff --git a/tests/playwright/deploys/plotly/test_plotly_app.py b/tests/playwright/deploys/plotly/test_plotly_app.py index d6b5d29fa..295cfdb82 100644 --- a/tests/playwright/deploys/plotly/test_plotly_app.py +++ b/tests/playwright/deploys/plotly/test_plotly_app.py @@ -1,5 +1,4 @@ import pytest -from playwright.sync_api import Page, expect from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -7,6 +6,8 @@ skip_if_not_chrome, ) +from shiny.test import Page, expect + TIMEOUT = 2 * 60 * 1000 app_url = create_deploys_app_url_fixture("example_deploy_app_A") diff --git a/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py b/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py index be697f413..6429d5000 100644 --- a/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py +++ b/tests/playwright/deploys/shiny-client-console-error/test_shiny_client_error.py @@ -1,5 +1,4 @@ import pytest -from playwright.sync_api import Page, expect from utils.deploy_utils import ( create_deploys_app_url_fixture, reruns, @@ -7,6 +6,8 @@ skip_if_not_chrome, ) +from shiny.test import Page, expect + app_url = create_deploys_app_url_fixture("shiny_client_console_error") diff --git a/tests/playwright/examples/example_apps.py b/tests/playwright/examples/example_apps.py index a5f0b54f0..68f6dcffc 100644 --- a/tests/playwright/examples/example_apps.py +++ b/tests/playwright/examples/example_apps.py @@ -4,9 +4,10 @@ from pathlib import PurePath from typing import Literal -from conftest import run_shiny_app from playwright.sync_api import ConsoleMessage, Page +from shiny.test._conftest import run_shiny_app + here_tests_e2e_examples = PurePath(__file__).parent pyshiny_root = here_tests_e2e_examples.parent.parent.parent diff --git a/tests/playwright/examples/test_api_examples.py b/tests/playwright/examples/test_api_examples.py index 475dda70a..a7bb2e205 100644 --- a/tests/playwright/examples/test_api_examples.py +++ b/tests/playwright/examples/test_api_examples.py @@ -1,6 +1,7 @@ import pytest from example_apps import get_apps, reruns, reruns_delay, validate_example -from playwright.sync_api import Page + +from shiny.test import Page @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py index 60117d7df..55ebf5f79 100644 --- a/tests/playwright/examples/test_examples.py +++ b/tests/playwright/examples/test_examples.py @@ -1,6 +1,7 @@ import pytest from example_apps import get_apps, reruns, reruns_delay, validate_example -from playwright.sync_api import Page + +from shiny.test import Page @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) diff --git a/tests/playwright/examples/test_shiny_create.py b/tests/playwright/examples/test_shiny_create.py index 83da63850..84aebdf92 100644 --- a/tests/playwright/examples/test_shiny_create.py +++ b/tests/playwright/examples/test_shiny_create.py @@ -4,9 +4,9 @@ import pytest from example_apps import get_apps, reruns, reruns_delay, validate_example -from playwright.sync_api import Page from shiny._main import app_template_choices +from shiny.test import Page def subprocess_create( diff --git a/tests/playwright/shiny/_internal/test_e2e_regex_matching.py b/tests/playwright/shiny/_internal/test_e2e_regex_matching.py index 9a80cad84..344105c78 100644 --- a/tests/playwright/shiny/_internal/test_e2e_regex_matching.py +++ b/tests/playwright/shiny/_internal/test_e2e_regex_matching.py @@ -1,6 +1,6 @@ import re -from controls import _attr_match_str, _style_match_str, _xpath_match_str +from shiny.test._controls import _attr_match_str, _style_match_str, _xpath_match_str def test_style_match_str() -> None: diff --git a/tests/playwright/shiny/async/test_async.py b/tests/playwright/shiny/async/test_async.py index 4e534ce7f..909bcaf9f 100644 --- a/tests/playwright/shiny/async/test_async.py +++ b/tests/playwright/shiny/async/test_async.py @@ -1,9 +1,8 @@ # See https://github.com/microsoft/playwright-python/issues/1532 # pyright: reportUnknownMemberType=false -from conftest import ShinyAppProc -from controls import InputActionButton, InputTextArea, OutputTextVerbatim -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputActionButton, InputTextArea, OutputTextVerbatim def test_async_app(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py b/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py index b569d0de7..bd6b1b09f 100644 --- a/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py +++ b/tests/playwright/shiny/bugs/0648-update-slider-datetime-value/test_update_slider_datetime_value.py @@ -2,9 +2,8 @@ from typing import Optional -from conftest import ShinyAppProc -from controls import InputActionButton, InputSlider, OutputTextVerbatim -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputActionButton, InputSlider, OutputTextVerbatim def test_slider_app(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py b/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py index e376d16ed..6e29d09e7 100644 --- a/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py +++ b/tests/playwright/shiny/bugs/0666-sidebar/test_sidebar_colors.py @@ -1,9 +1,9 @@ from __future__ import annotations from colors import bg_color, fg_color -from conftest import ShinyAppProc -from controls import Sidebar, _expect_class_value -from playwright.sync_api import Page, expect + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import Sidebar, _expect_class_value def test_colors_are_rgb() -> None: diff --git a/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py b/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py index ef991002f..cb63f335c 100644 --- a/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py +++ b/tests/playwright/shiny/bugs/0676-row-selection/test_0676_row_selection.py @@ -1,7 +1,6 @@ from __future__ import annotations -from conftest import ShinyAppProc -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect def test_row_selection(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py b/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py index 4a0ad4210..e6f8bb53a 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/mod_state.py @@ -2,8 +2,8 @@ import datetime -from controls import OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page +from shiny.test._controls import OutputTextVerbatim def expect_state( diff --git a/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py b/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py index be00085ae..703d53179 100644 --- a/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py +++ b/tests/playwright/shiny/bugs/0696-resolve-id/test_0696_resolve_id.py @@ -6,8 +6,12 @@ from pathlib import Path import pytest -from conftest import ShinyAppProc -from controls import ( +from examples.example_apps import reruns, reruns_delay +from mod_state import expect_default_mod_state, expect_mod_state + +from shiny._utils import guess_mime_type +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import ( DownloadButton, DownloadLink, InputActionButton, @@ -32,11 +36,6 @@ OutputTextVerbatim, OutputUi, ) -from examples.example_apps import reruns, reruns_delay -from mod_state import expect_default_mod_state, expect_mod_state -from playwright.sync_api import Page - -from shiny._utils import guess_mime_type img_path = Path(__file__).parent / "imgs" penguin_imgs = [str(img_path / img) for img in os.listdir(img_path)] diff --git a/tests/playwright/shiny/bugs/1345-render-data-frame-input/test_1345_input_selected_rows.py b/tests/playwright/shiny/bugs/1345-render-data-frame-input/test_1345_input_selected_rows.py index 80c2d6b17..822d72420 100644 --- a/tests/playwright/shiny/bugs/1345-render-data-frame-input/test_1345_input_selected_rows.py +++ b/tests/playwright/shiny/bugs/1345-render-data-frame-input/test_1345_input_selected_rows.py @@ -1,8 +1,7 @@ from __future__ import annotations -from conftest import ShinyAppProc -from controls import OutputDataFrame, OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputDataFrame, OutputTextVerbatim def test_row_selection(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/bugs/1351-render-data-frame-selected/test_1351_selected_row.py b/tests/playwright/shiny/bugs/1351-render-data-frame-selected/test_1351_selected_row.py index f98177f5a..5c2374c76 100644 --- a/tests/playwright/shiny/bugs/1351-render-data-frame-selected/test_1351_selected_row.py +++ b/tests/playwright/shiny/bugs/1351-render-data-frame-selected/test_1351_selected_row.py @@ -1,8 +1,7 @@ from __future__ import annotations -from conftest import ShinyAppProc -from controls import InputActionButton, OutputDataFrame, OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputActionButton, OutputDataFrame, OutputTextVerbatim def test_row_selection(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/accordion/test_accordion.py b/tests/playwright/shiny/components/accordion/test_accordion.py index f47786728..893bdcb03 100644 --- a/tests/playwright/shiny/components/accordion/test_accordion.py +++ b/tests/playwright/shiny/components/accordion/test_accordion.py @@ -1,8 +1,8 @@ import pytest -from conftest import ShinyAppProc -from controls import Accordion, InputActionButton, OutputTextVerbatim from examples.example_apps import reruns, reruns_delay -from playwright.sync_api import Page + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import Accordion, InputActionButton, OutputTextVerbatim @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) diff --git a/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py b/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py index 8156e719b..8a42d8b2d 100644 --- a/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py +++ b/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py @@ -1,9 +1,8 @@ import os from urllib.parse import urlparse -from conftest import ShinyAppProc -from controls import InputRadioButtons, InputTaskButton, OutputTextVerbatim -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputRadioButtons, InputTaskButton, OutputTextVerbatim def get_spinner_computed_property( diff --git a/tests/playwright/shiny/components/card-input/test_card-input.py b/tests/playwright/shiny/components/card-input/test_card-input.py index 1cc794aff..375dbbe9d 100644 --- a/tests/playwright/shiny/components/card-input/test_card-input.py +++ b/tests/playwright/shiny/components/card-input/test_card-input.py @@ -1,9 +1,10 @@ from pathlib import Path import pytest -from conftest import run_shiny_app -from controls import Card, OutputCode, ValueBox -from playwright.sync_api import Page + +from shiny.test import Page +from shiny.test._conftest import run_shiny_app +from shiny.test._controls import Card, OutputCode, ValueBox @pytest.mark.parametrize( diff --git a/tests/playwright/shiny/components/data_frame/example/test_data_frame.py b/tests/playwright/shiny/components/data_frame/example/test_data_frame.py index 36bd187d4..3c51171d4 100644 --- a/tests/playwright/shiny/components/data_frame/example/test_data_frame.py +++ b/tests/playwright/shiny/components/data_frame/example/test_data_frame.py @@ -5,10 +5,12 @@ from typing import Any, Callable import pytest -from conftest import ShinyAppProc, create_example_fixture, expect_to_change -from controls import InputSelect, InputSwitch +from conftest import create_example_fixture from examples.example_apps import reruns, reruns_delay -from playwright.sync_api import Locator, Page, expect + +from shiny.test import Locator, Page, ShinyAppProc, expect +from shiny.test._controls import InputSelect, InputSwitch +from shiny.test._expect import expect_to_change data_frame_app = create_example_fixture("dataframe") diff --git a/tests/playwright/shiny/components/data_frame/html_columns_df/df_organization/test_df_organization.py b/tests/playwright/shiny/components/data_frame/html_columns_df/df_organization/test_df_organization.py index f0400a920..45bcc3676 100644 --- a/tests/playwright/shiny/components/data_frame/html_columns_df/df_organization/test_df_organization.py +++ b/tests/playwright/shiny/components/data_frame/html_columns_df/df_organization/test_df_organization.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import InputActionButton, OutputCode, OutputDataFrame -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputActionButton, OutputCode, OutputDataFrame def test_dataframe_organization_methods(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/app.py b/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/app.py index 4219896a4..853c23503 100644 --- a/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/app.py +++ b/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/app.py @@ -4,9 +4,9 @@ from shiny import App, Inputs, Outputs, Session, render, ui df = pd.DataFrame( - sns.load_dataset( + sns.load_dataset( # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] "iris" - ) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] + ) ) app_ui = ui.page_fluid( ui.h2("Iris Dataset"), diff --git a/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/test_tabbing.py b/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/test_tabbing.py index 9005b05f3..d193c7bf8 100644 --- a/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/test_tabbing.py +++ b/tests/playwright/shiny/components/data_frame/html_columns_df/tabbing/test_tabbing.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputDataFrame -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputDataFrame def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/data_frame/html_columns_df/test_html_columns.py b/tests/playwright/shiny/components/data_frame/html_columns_df/test_html_columns.py index deb9ec6db..c6834979d 100644 --- a/tests/playwright/shiny/components/data_frame/html_columns_df/test_html_columns.py +++ b/tests/playwright/shiny/components/data_frame/html_columns_df/test_html_columns.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import InputActionButton, OutputDataFrame, OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputActionButton, OutputDataFrame, OutputTextVerbatim def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/data_frame/row_selection/test_row_selection.py b/tests/playwright/shiny/components/data_frame/row_selection/test_row_selection.py index 5addfa2a0..bf8b6ee40 100644 --- a/tests/playwright/shiny/components/data_frame/row_selection/test_row_selection.py +++ b/tests/playwright/shiny/components/data_frame/row_selection/test_row_selection.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import InputActionButton, OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputActionButton, OutputTextVerbatim def expect_row_selection(page: Page, prefix_main: str, prefix_secondary: str): diff --git a/tests/playwright/shiny/components/data_frame/validate_data_edit_mode/test_validate_data_edit_mode.py b/tests/playwright/shiny/components/data_frame/validate_data_edit_mode/test_validate_data_edit_mode.py index 6d26e966b..b450fcd5d 100644 --- a/tests/playwright/shiny/components/data_frame/validate_data_edit_mode/test_validate_data_edit_mode.py +++ b/tests/playwright/shiny/components/data_frame/validate_data_edit_mode/test_validate_data_edit_mode.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputDataFrame -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputDataFrame def test_validate_data_edit_mode(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/data_frame/validate_data_save/test_validate_data_save.py b/tests/playwright/shiny/components/data_frame/validate_data_save/test_validate_data_save.py index 1a7c19b78..013556d39 100644 --- a/tests/playwright/shiny/components/data_frame/validate_data_save/test_validate_data_save.py +++ b/tests/playwright/shiny/components/data_frame/validate_data_save/test_validate_data_save.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputDataFrame -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputDataFrame def test_validate_data_edit_mode(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/layout_columns/test_layout_columns.py b/tests/playwright/shiny/components/layout_columns/test_layout_columns.py index 38f421c22..652dda2e8 100644 --- a/tests/playwright/shiny/components/layout_columns/test_layout_columns.py +++ b/tests/playwright/shiny/components/layout_columns/test_layout_columns.py @@ -2,8 +2,9 @@ from typing import TypeVar -from conftest import ShinyAppProc, create_doc_example_core_fixture -from playwright.sync_api import Page +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc T = TypeVar("T") diff --git a/tests/playwright/shiny/components/nav/test_nav.py b/tests/playwright/shiny/components/nav/test_nav.py index 546f81090..7994ead13 100644 --- a/tests/playwright/shiny/components/nav/test_nav.py +++ b/tests/playwright/shiny/components/nav/test_nav.py @@ -3,8 +3,9 @@ from dataclasses import dataclass import pytest -from conftest import ShinyAppProc -from controls import ( + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import ( LayoutNavSetBar, LayoutNavSetCardPill, LayoutNavSetCardTab, @@ -14,7 +15,6 @@ LayoutNavsetTab, LayoutNavSetUnderline, ) -from playwright.sync_api import Page @pytest.mark.skip_browser("webkit") diff --git a/tests/playwright/shiny/components/navset_hidden/test_nav_hidden.py b/tests/playwright/shiny/components/navset_hidden/test_nav_hidden.py index 650fd7bba..4d7a05510 100644 --- a/tests/playwright/shiny/components/navset_hidden/test_nav_hidden.py +++ b/tests/playwright/shiny/components/navset_hidden/test_nav_hidden.py @@ -1,7 +1,6 @@ # import pytest -from conftest import ShinyAppProc -from controls import InputRadioButtons, LayoutNavSetHidden -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputRadioButtons, LayoutNavSetHidden def test_navset_hidden(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/popover/test_popover.py b/tests/playwright/shiny/components/popover/test_popover.py index dc6bd74d0..90115064e 100644 --- a/tests/playwright/shiny/components/popover/test_popover.py +++ b/tests/playwright/shiny/components/popover/test_popover.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import Popover -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import Popover def test_popover(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/test_sidebar.py b/tests/playwright/shiny/components/test_sidebar.py index 5c3371cf1..de308f30c 100644 --- a/tests/playwright/shiny/components/test_sidebar.py +++ b/tests/playwright/shiny/components/test_sidebar.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import OutputTextVerbatim, Sidebar -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import OutputTextVerbatim, Sidebar app = create_doc_example_core_fixture("sidebar") diff --git a/tests/playwright/shiny/components/tooltip/test_tooltip.py b/tests/playwright/shiny/components/tooltip/test_tooltip.py index 4ea32e56b..bf9dff4aa 100644 --- a/tests/playwright/shiny/components/tooltip/test_tooltip.py +++ b/tests/playwright/shiny/components/tooltip/test_tooltip.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import Tooltip -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import Tooltip def test_tooltip(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py b/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py index 776571cdf..6947ddf0d 100644 --- a/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py +++ b/tests/playwright/shiny/components/value_box/kitchensink/test_valuebox_ks.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import ValueBox, expect_to_have_class -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import ValueBox, expect_to_have_class def get_value_box_bg_color(value_box: ValueBox) -> str: diff --git a/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py b/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py index 0458f1c87..cb4202986 100644 --- a/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py +++ b/tests/playwright/shiny/components/value_box/smoke/test_valuebox.py @@ -1,7 +1,7 @@ import pytest -from conftest import ShinyAppProc -from controls import ValueBox -from playwright.sync_api import Page + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import ValueBox @pytest.mark.parametrize("value_box_id", ["valuebox1", "valuebox2"]) diff --git a/tests/playwright/shiny/default-render-ui/test_default_render_ui.py b/tests/playwright/shiny/default-render-ui/test_default_render_ui.py index 283557aec..68aec6da5 100644 --- a/tests/playwright/shiny/default-render-ui/test_default_render_ui.py +++ b/tests/playwright/shiny/default-render-ui/test_default_render_ui.py @@ -1,5 +1,4 @@ -from conftest import ShinyAppProc -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect def test_implicit_register(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py b/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py index ca16acb10..e140dfa82 100644 --- a/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py +++ b/tests/playwright/shiny/deprecated/output_transformer/test_output_transformer_example.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputTextVerbatim def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py b/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py index a11130dca..6a7c24d8e 100644 --- a/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py +++ b/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import Card -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import Card def get_body_tag_name(card: Card) -> str: diff --git a/tests/playwright/shiny/experimental/card/test_card.py b/tests/playwright/shiny/experimental/card/test_card.py index 6c49f4e0b..7a154a5ae 100644 --- a/tests/playwright/shiny/experimental/card/test_card.py +++ b/tests/playwright/shiny/experimental/card/test_card.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import Card -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import Card def test_card(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/implicit-register/test_implicit_register.py b/tests/playwright/shiny/implicit-register/test_implicit_register.py index 668274dbd..6c1420163 100644 --- a/tests/playwright/shiny/implicit-register/test_implicit_register.py +++ b/tests/playwright/shiny/implicit-register/test_implicit_register.py @@ -1,5 +1,4 @@ -from conftest import ShinyAppProc -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect def test_implicit_register(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/inputs/input_file/app.py b/tests/playwright/shiny/inputs/input_file/app.py index 4541df7c0..369d7f90a 100644 --- a/tests/playwright/shiny/inputs/input_file/app.py +++ b/tests/playwright/shiny/inputs/input_file/app.py @@ -40,8 +40,8 @@ def summary(): row_count = df.shape[0] column_count = df.shape[1] names: list[str] = ( - df.columns.tolist() - ) # pyright: ignore[reportUnknownMemberType] + df.columns.tolist() # pyright: ignore[reportUnknownMemberType] + ) column_names = ", ".join(str(name) for name in names) # Create a new DataFrame to display the information diff --git a/tests/playwright/shiny/inputs/input_file/test_input_file.py b/tests/playwright/shiny/inputs/input_file/test_input_file.py index 953c1ea35..9fd5a93a1 100644 --- a/tests/playwright/shiny/inputs/input_file/test_input_file.py +++ b/tests/playwright/shiny/inputs/input_file/test_input_file.py @@ -1,8 +1,8 @@ from __future__ import annotations -from conftest import ShinyAppProc -from controls import InputFile, OutputTable, OutputTextVerbatim -from playwright.sync_api import FilePayload, Page, expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputFile, OutputTable, OutputTextVerbatim +from shiny.test.playwright import FilePayload def test_input_file_kitchen(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py b/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py index 24f5b265b..3431bb39d 100644 --- a/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py +++ b/tests/playwright/shiny/inputs/input_radio_checkbox_group/test_input_radio_checkbox_group_app.py @@ -1,8 +1,7 @@ from __future__ import annotations -from conftest import ShinyAppProc -from controls import InputCheckboxGroup, InputRadioButtons, PatternOrStr -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputCheckboxGroup, InputRadioButtons, PatternOrStr def test_input_checkbox_group_kitchen(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py b/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py index bb109e7f1..00bfa0325 100644 --- a/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py +++ b/tests/playwright/shiny/inputs/input_slider/test_input_slider_app.py @@ -1,10 +1,8 @@ import re import time -from conftest import ShinyAppProc -from controls import InputSlider, InputSliderRange, OutputTextVerbatim -from playwright.sync_api import Page - +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputSlider, InputSliderRange, OutputTextVerbatim from shiny.types import MISSING diff --git a/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py b/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py index f90ce3d5b..05942c387 100644 --- a/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py +++ b/tests/playwright/shiny/inputs/input_task_button/test_input_task_button.py @@ -2,9 +2,8 @@ import time -from conftest import ShinyAppProc -from controls import InputNumeric, InputTaskButton, OutputText -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputNumeric, InputTaskButton, OutputText def click_extended_task_button( diff --git a/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py b/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py index 85da00177..026c23e10 100644 --- a/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py +++ b/tests/playwright/shiny/inputs/input_task_button2/test_input_task_button2.py @@ -1,8 +1,7 @@ from __future__ import annotations -from conftest import ShinyAppProc -from controls import InputTaskButton, OutputText -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputTaskButton, OutputText def click_extended_task_button( diff --git a/tests/playwright/shiny/inputs/test_input_action_button_link.py b/tests/playwright/shiny/inputs/test_input_action_button_link.py index 66edf7682..f51058433 100644 --- a/tests/playwright/shiny/inputs/test_input_action_button_link.py +++ b/tests/playwright/shiny/inputs/test_input_action_button_link.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputActionButton, InputActionLink -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputActionButton, InputActionLink app = create_doc_example_core_fixture("update_action_button") diff --git a/tests/playwright/shiny/inputs/test_input_checkbox.py b/tests/playwright/shiny/inputs/test_input_checkbox.py index 687c5aca2..3bf1795b1 100644 --- a/tests/playwright/shiny/inputs/test_input_checkbox.py +++ b/tests/playwright/shiny/inputs/test_input_checkbox.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputCheckbox, OutputUi -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputCheckbox, OutputUi app = create_doc_example_core_fixture("input_checkbox") diff --git a/tests/playwright/shiny/inputs/test_input_checkbox_group.py b/tests/playwright/shiny/inputs/test_input_checkbox_group.py index aab1262c2..c1df2fbaf 100644 --- a/tests/playwright/shiny/inputs/test_input_checkbox_group.py +++ b/tests/playwright/shiny/inputs/test_input_checkbox_group.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputCheckboxGroup -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputCheckboxGroup app = create_doc_example_core_fixture("input_checkbox_group") diff --git a/tests/playwright/shiny/inputs/test_input_dark_mode.py b/tests/playwright/shiny/inputs/test_input_dark_mode.py index d5c793566..556aaee4d 100644 --- a/tests/playwright/shiny/inputs/test_input_dark_mode.py +++ b/tests/playwright/shiny/inputs/test_input_dark_mode.py @@ -1,8 +1,9 @@ from __future__ import annotations -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputActionButton, InputDarkMode, LayoutNavSetBar -from playwright.sync_api import Page +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputActionButton, InputDarkMode, LayoutNavSetBar app = create_doc_example_core_fixture("input_dark_mode") diff --git a/tests/playwright/shiny/inputs/test_input_date.py b/tests/playwright/shiny/inputs/test_input_date.py index ceac65dd1..fbf7887c0 100644 --- a/tests/playwright/shiny/inputs/test_input_date.py +++ b/tests/playwright/shiny/inputs/test_input_date.py @@ -4,9 +4,10 @@ import typing from typing import Literal -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputDate -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputDate app = create_doc_example_core_fixture("input_date") diff --git a/tests/playwright/shiny/inputs/test_input_date_range.py b/tests/playwright/shiny/inputs/test_input_date_range.py index 778f6d1d7..dd704b224 100644 --- a/tests/playwright/shiny/inputs/test_input_date_range.py +++ b/tests/playwright/shiny/inputs/test_input_date_range.py @@ -4,9 +4,10 @@ import typing from typing import Literal -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputDateRange -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputDateRange app = create_doc_example_core_fixture("input_date_range") diff --git a/tests/playwright/shiny/inputs/test_input_numeric.py b/tests/playwright/shiny/inputs/test_input_numeric.py index 503be64c3..2f7be1146 100644 --- a/tests/playwright/shiny/inputs/test_input_numeric.py +++ b/tests/playwright/shiny/inputs/test_input_numeric.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputNumeric, OutputTextVerbatim -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputNumeric, OutputTextVerbatim app = create_doc_example_core_fixture("input_numeric") diff --git a/tests/playwright/shiny/inputs/test_input_password.py b/tests/playwright/shiny/inputs/test_input_password.py index ea83fe17d..1d3f40103 100644 --- a/tests/playwright/shiny/inputs/test_input_password.py +++ b/tests/playwright/shiny/inputs/test_input_password.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputActionButton, InputPassword, OutputTextVerbatim -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputActionButton, InputPassword, OutputTextVerbatim app = create_doc_example_core_fixture("input_password") diff --git a/tests/playwright/shiny/inputs/test_input_radio_buttons.py b/tests/playwright/shiny/inputs/test_input_radio_buttons.py index b19d888b0..48199936b 100644 --- a/tests/playwright/shiny/inputs/test_input_radio_buttons.py +++ b/tests/playwright/shiny/inputs/test_input_radio_buttons.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputRadioButtons -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputRadioButtons app = create_doc_example_core_fixture("input_radio_buttons") diff --git a/tests/playwright/shiny/inputs/test_input_select.py b/tests/playwright/shiny/inputs/test_input_select.py index 9d4316912..9b3b67afa 100644 --- a/tests/playwright/shiny/inputs/test_input_select.py +++ b/tests/playwright/shiny/inputs/test_input_select.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputSelect -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputSelect app = create_doc_example_core_fixture("input_select") diff --git a/tests/playwright/shiny/inputs/test_input_selectize.py b/tests/playwright/shiny/inputs/test_input_selectize.py index f725b3752..546fa2e3e 100644 --- a/tests/playwright/shiny/inputs/test_input_selectize.py +++ b/tests/playwright/shiny/inputs/test_input_selectize.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputSelectize -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputSelectize app = create_doc_example_core_fixture("input_selectize") diff --git a/tests/playwright/shiny/inputs/test_input_slider.py b/tests/playwright/shiny/inputs/test_input_slider.py index 5da4cfaac..79fb9e9ec 100644 --- a/tests/playwright/shiny/inputs/test_input_slider.py +++ b/tests/playwright/shiny/inputs/test_input_slider.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputSlider, OutputTextVerbatim -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputSlider, OutputTextVerbatim slider_app = create_doc_example_core_fixture("input_slider") template_app = create_doc_example_core_fixture("template") diff --git a/tests/playwright/shiny/inputs/test_input_switch.py b/tests/playwright/shiny/inputs/test_input_switch.py index 0fc16a042..5b6bf823a 100644 --- a/tests/playwright/shiny/inputs/test_input_switch.py +++ b/tests/playwright/shiny/inputs/test_input_switch.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputSwitch, OutputUi -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputSwitch, OutputUi app = create_doc_example_core_fixture("input_switch") diff --git a/tests/playwright/shiny/inputs/test_input_text.py b/tests/playwright/shiny/inputs/test_input_text.py index d2b045c69..131c73c50 100644 --- a/tests/playwright/shiny/inputs/test_input_text.py +++ b/tests/playwright/shiny/inputs/test_input_text.py @@ -1,8 +1,9 @@ import re -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputText, OutputTextVerbatim -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputText, OutputTextVerbatim app = create_doc_example_core_fixture("input_text") diff --git a/tests/playwright/shiny/inputs/test_input_text_area.py b/tests/playwright/shiny/inputs/test_input_text_area.py index 40ab6ccd0..c4531a79b 100644 --- a/tests/playwright/shiny/inputs/test_input_text_area.py +++ b/tests/playwright/shiny/inputs/test_input_text_area.py @@ -1,8 +1,9 @@ import re -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputTextArea, OutputTextVerbatim -from playwright.sync_api import Locator, Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Locator, Page, ShinyAppProc, expect +from shiny.test._controls import InputTextArea, OutputTextVerbatim app = create_doc_example_core_fixture("input_text_area") diff --git a/tests/playwright/shiny/inputs/test_inputs_update.py b/tests/playwright/shiny/inputs/test_inputs_update.py index bd52d1324..eb927b8f4 100644 --- a/tests/playwright/shiny/inputs/test_inputs_update.py +++ b/tests/playwright/shiny/inputs/test_inputs_update.py @@ -1,7 +1,9 @@ # pyright: reportUnknownMemberType=false -from conftest import ShinyAppProc, create_example_fixture -from controls import ( +from conftest import create_example_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import ( MISSING, InputCheckbox, InputCheckboxGroup, @@ -14,7 +16,6 @@ InputSliderRange, InputText, ) -from playwright.sync_api import Page, expect inputs_update_app = create_example_fixture("inputs-update") diff --git a/tests/playwright/shiny/module-conditional/test_module_conditional.py b/tests/playwright/shiny/module-conditional/test_module_conditional.py index c83a1fecb..967c386e3 100644 --- a/tests/playwright/shiny/module-conditional/test_module_conditional.py +++ b/tests/playwright/shiny/module-conditional/test_module_conditional.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import InputCheckbox -from playwright.sync_api import Page, expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputCheckbox def test_async_app(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/outputs/test_output_image.py b/tests/playwright/shiny/outputs/test_output_image.py index 76c18a3b9..6a08312d6 100644 --- a/tests/playwright/shiny/outputs/test_output_image.py +++ b/tests/playwright/shiny/outputs/test_output_image.py @@ -1,8 +1,9 @@ import re -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import OutputImage -from playwright.sync_api import Page +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputImage app = create_doc_example_core_fixture("output_image") diff --git a/tests/playwright/shiny/outputs/test_output_plot.py b/tests/playwright/shiny/outputs/test_output_plot.py index 5999badf5..572b557db 100644 --- a/tests/playwright/shiny/outputs/test_output_plot.py +++ b/tests/playwright/shiny/outputs/test_output_plot.py @@ -1,8 +1,9 @@ import re -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import OutputPlot -from playwright.sync_api import Page +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputPlot app = create_doc_example_core_fixture("output_plot") diff --git a/tests/playwright/shiny/outputs/test_output_table.py b/tests/playwright/shiny/outputs/test_output_table.py index 72e55efdb..4f35af1de 100644 --- a/tests/playwright/shiny/outputs/test_output_table.py +++ b/tests/playwright/shiny/outputs/test_output_table.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import OutputTable -from playwright.sync_api import Page +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputTable app = create_doc_example_core_fixture("output_table") diff --git a/tests/playwright/shiny/outputs/test_output_text.py b/tests/playwright/shiny/outputs/test_output_text.py index bca59a0a4..b47d281fb 100644 --- a/tests/playwright/shiny/outputs/test_output_text.py +++ b/tests/playwright/shiny/outputs/test_output_text.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputText, OutputText, OutputTextVerbatim -from playwright.sync_api import Page +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputText, OutputText, OutputTextVerbatim app = create_doc_example_core_fixture("output_text") diff --git a/tests/playwright/shiny/outputs/test_output_ui.py b/tests/playwright/shiny/outputs/test_output_ui.py index 44508b6d7..80ce1be7a 100644 --- a/tests/playwright/shiny/outputs/test_output_ui.py +++ b/tests/playwright/shiny/outputs/test_output_ui.py @@ -1,6 +1,7 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from controls import InputActionButton, InputSlider, InputText, OutputUi -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import InputActionButton, InputSlider, InputText, OutputUi app = create_doc_example_core_fixture("output_ui") diff --git a/tests/playwright/shiny/plot-sizing/test_plot_sizing.py b/tests/playwright/shiny/plot-sizing/test_plot_sizing.py index aef615d6c..b1dd12625 100644 --- a/tests/playwright/shiny/plot-sizing/test_plot_sizing.py +++ b/tests/playwright/shiny/plot-sizing/test_plot_sizing.py @@ -1,8 +1,7 @@ import re -from conftest import ShinyAppProc -from controls import OutputPlot -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputPlot def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/server/output_transformer/test_output_transformer_async.py b/tests/playwright/shiny/server/output_transformer/test_output_transformer_async.py index 11230bff1..fed2f9c0d 100644 --- a/tests/playwright/shiny/server/output_transformer/test_output_transformer_async.py +++ b/tests/playwright/shiny/server/output_transformer/test_output_transformer_async.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputTextVerbatim def test_output_transformer(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/server/reactive_event/test_reactive_event.py b/tests/playwright/shiny/server/reactive_event/test_reactive_event.py index 4036bba5d..2996608e4 100644 --- a/tests/playwright/shiny/server/reactive_event/test_reactive_event.py +++ b/tests/playwright/shiny/server/reactive_event/test_reactive_event.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import InputActionButton, OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import InputActionButton, OutputTextVerbatim def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/session/flush/test_on_flush.py b/tests/playwright/shiny/session/flush/test_on_flush.py index 4d14a3321..90dfd45e8 100644 --- a/tests/playwright/shiny/session/flush/test_on_flush.py +++ b/tests/playwright/shiny/session/flush/test_on_flush.py @@ -1,6 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputTextVerbatim -from playwright.sync_api import Page +from shiny.test import Page, ShinyAppProc +from shiny.test._controls import OutputTextVerbatim def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None: diff --git a/tests/playwright/shiny/shiny-express/hold/test_hold.py b/tests/playwright/shiny/shiny-express/hold/test_hold.py index 77df73f52..82d6d8b6f 100644 --- a/tests/playwright/shiny/shiny-express/hold/test_hold.py +++ b/tests/playwright/shiny/shiny-express/hold/test_hold.py @@ -1,7 +1,5 @@ -from conftest import ShinyAppProc -from controls import OutputTextVerbatim -from playwright.sync_api import Page -from playwright.sync_api import expect as playwright_expect +from shiny.test import Page, ShinyAppProc, expect +from shiny.test._controls import OutputTextVerbatim def test_express_page_fluid(page: Page, local_app: ShinyAppProc) -> None: @@ -10,5 +8,5 @@ def test_express_page_fluid(page: Page, local_app: ShinyAppProc) -> None: txt = OutputTextVerbatim(page, "visible") txt.expect_value("40") - playwright_expect(page.locator("#visible")).to_have_count(1) - playwright_expect(page.locator("#hidden")).to_have_count(0) + expect(page.locator("#visible")).to_have_count(1) + expect(page.locator("#hidden")).to_have_count(0) diff --git a/tests/playwright/shiny/shiny-express/render_express/test_render_express.py b/tests/playwright/shiny/shiny-express/render_express/test_render_express.py index 5abd75a4e..feaba3dd7 100644 --- a/tests/playwright/shiny/shiny-express/render_express/test_render_express.py +++ b/tests/playwright/shiny/shiny-express/render_express/test_render_express.py @@ -1,5 +1,6 @@ -from conftest import ShinyAppProc, create_doc_example_core_fixture -from playwright.sync_api import Page, expect +from conftest import create_doc_example_core_fixture + +from shiny.test import Page, ShinyAppProc, expect app = create_doc_example_core_fixture("render_express") diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index b41bb340d..a40676e92 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -11,7 +11,8 @@ import pytest import requests -from conftest import ScopeName, local_app_fixture_gen + +from shiny.test.fixture import ScopeName, local_app_fixture_gen is_interactive = hasattr(sys, "ps1") reruns = 1 if is_interactive else 3 From da7bdbbe11f35f559971d98da362c5a4a3e18775 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Fri, 24 May 2024 09:55:00 -0700 Subject: [PATCH 03/14] refactor imports for test_cpuinfo.py --- tests/playwright/examples/test_cpuinfo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/playwright/examples/test_cpuinfo.py b/tests/playwright/examples/test_cpuinfo.py index d8ba32437..4ab182321 100644 --- a/tests/playwright/examples/test_cpuinfo.py +++ b/tests/playwright/examples/test_cpuinfo.py @@ -2,8 +2,9 @@ import re -from conftest import ShinyAppProc, create_example_fixture -from playwright.sync_api import Page, expect +from conftest import create_example_fixture + +from shiny.test import Page, ShinyAppProc, expect cpuinfo_app = create_example_fixture("cpuinfo") From 9323c34d95c1895d31adb5c88a218a3067c82079 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 28 May 2024 09:26:58 -0700 Subject: [PATCH 04/14] Sort imports --- .../test_1390_selected_row_filtered.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/playwright/shiny/bugs/1390-df-selected-row-filtered/test_1390_selected_row_filtered.py b/tests/playwright/shiny/bugs/1390-df-selected-row-filtered/test_1390_selected_row_filtered.py index 170012c5a..90f26fc70 100644 --- a/tests/playwright/shiny/bugs/1390-df-selected-row-filtered/test_1390_selected_row_filtered.py +++ b/tests/playwright/shiny/bugs/1390-df-selected-row-filtered/test_1390_selected_row_filtered.py @@ -2,10 +2,11 @@ import platform -from conftest import ShinyAppProc -from controls import OutputCode, OutputDataFrame from playwright.sync_api import Page, expect +from shiny.test import ShinyAppProc +from shiny.test._controls import OutputCode, OutputDataFrame + def test_row_selection(page: Page, local_app: ShinyAppProc) -> None: page.goto(local_app.url) From 44903e6569ac43322585bd7a4f13d848695fe4d5 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Wed, 29 May 2024 21:43:54 -0700 Subject: [PATCH 05/14] Address code review comments --- shiny/test/__init__.py | 3 +- shiny/test/_conftest.py | 48 ++++++++++++++++++- shiny/test/_controls.py | 4 +- shiny/test/{playwright.py => _playwright.py} | 0 shiny/test/_pytest.py | 27 ++++++++++- shiny/test/fixture/_fixture.py | 21 +++++++- tests/playwright/README.md | 2 +- .../inputs/input_file/test_input_file.py | 2 +- 8 files changed, 98 insertions(+), 9 deletions(-) rename shiny/test/{playwright.py => _playwright.py} (100%) diff --git a/shiny/test/__init__.py b/shiny/test/__init__.py index 2b18a6b96..edf5ac33d 100644 --- a/shiny/test/__init__.py +++ b/shiny/test/__init__.py @@ -14,10 +14,11 @@ ) # from ._expect import expect_to_change -from .playwright import Page, expect, Locator +from ._playwright import Page, expect, Locator __all__ = ( "expect", + # TODO-future: Find the proper location for these methods to be returned # "run_shiny_app", # "expect_to_change", "ShinyAppProc", diff --git a/shiny/test/_conftest.py b/shiny/test/_conftest.py index 9d425be81..2e21d7148 100644 --- a/shiny/test/_conftest.py +++ b/shiny/test/_conftest.py @@ -54,6 +54,18 @@ def _run(self): self._cond.notify_all() def wait_for(self, predicate: Callable[[str], bool], timeoutSecs: float) -> bool: + """ + Wait until the predicate returns True for a line in the output. + + Parameters + ---------- + predicate + A function that takes a line of output and returns True if the line + satisfies the condition. + timeoutSecs + How long to wait for the predicate to return True before raising a + TimeoutError. + """ timeoutAt = datetime.datetime.now() + datetime.timedelta(seconds=timeoutSecs) pos = 0 with self._cond: @@ -100,6 +112,11 @@ def _run(self) -> None: self.proc.stderr.close() def close(self) -> None: + """ + Closes the connection and terminates the process. + + This method is responsible for closing the connection and terminating the process associated with it. + """ # from time import sleep # sleep(0.5) self.proc.terminate() @@ -116,12 +133,23 @@ def __exit__( self.close() def wait_until_ready(self, timeoutSecs: float) -> None: + """ + Waits until the shiny app is ready to serve requests. + + Parameters: + timeoutSecs (float): The maximum number of seconds to wait for the app to become ready. + + Raises: + ConnectionError: If there is an error while starting the shiny app. + TimeoutError: If the shiny app does not become ready within the specified timeout. + + """ error_lines: List[str] = [] def stderr_uvicorn(line: str) -> bool: error_lines.append(line) if "error while attempting to bind on address" in line: - raise ConnectionError(f"Error while staring shiny app: `{line}`") + raise ConnectionError(f"Error while starting shiny app: `{line}`") return "Uvicorn running on" in line if self.stderr.wait_for(stderr_uvicorn, timeoutSecs=timeoutSecs): @@ -142,6 +170,24 @@ def run_shiny_app( timeout_secs: float = 10, bufsize: int = 64 * 1024, ) -> ShinyAppProc: + """ + Run a Shiny app in a subprocess. + + Parameters + ---------- + app_file + The path to the Shiny app file. + port + The port to run the app on. If 0, a random port will be chosen. + cwd + The working directory to run the app in. + wait_for_start + If True, wait for the app to become ready before returning. + timeout_secs + The maximum number of seconds to wait for the app to become ready. + bufsize + The buffer size to use for stdout and stderr. + """ shiny_port = port if port != 0 else shiny._utils.random_port() child = subprocess.Popen( diff --git a/shiny/test/_controls.py b/shiny/test/_controls.py index 6d81b4e9e..59ff6cefb 100644 --- a/shiny/test/_controls.py +++ b/shiny/test/_controls.py @@ -18,8 +18,8 @@ assert_type, # pyright: ignore[reportPrivateImportUsage] ) from ..types import MISSING, MISSING_TYPE -from .playwright import FilePayload, FloatRect, Locator, Page, Position -from .playwright import expect as playwright_expect +from ._playwright import FilePayload, FloatRect, Locator, Page, Position +from ._playwright import expect as playwright_expect """ Questions: diff --git a/shiny/test/playwright.py b/shiny/test/_playwright.py similarity index 100% rename from shiny/test/playwright.py rename to shiny/test/_playwright.py diff --git a/shiny/test/_pytest.py b/shiny/test/_pytest.py index 7a6643e76..8e134f2f0 100644 --- a/shiny/test/_pytest.py +++ b/shiny/test/_pytest.py @@ -6,14 +6,24 @@ import pytest from ._conftest import ShinyAppProc +from ._playwright import BrowserContext, Page from .fixture import local_app_fixture_gen -from .playwright import BrowserContext, Page # Make a single page fixture that can be used by all tests @pytest.fixture(scope="session") # By using a single page, the browser is only launched once and all tests run in the same tab / page. def session_page(browser: BrowserContext) -> Page: + """ + Create a new page within the given browser context. + + Parameters: + browser (BrowserContext): The browser context in which to create the new page. + + Returns: + Page: The newly created page. + + """ return browser.new_page() @@ -22,6 +32,15 @@ def session_page(browser: BrowserContext) -> Page: # It is not perfect, but it is faster than making a new page for each test. # This must be done before each test def page(session_page: Page) -> Page: + """ + Reset the given page to a known state before each test. + The page is built on the session_page, which is maintained over the full session. + The page will visit "about:blank" to reset between apps. + The default viewport size is set to 1920 x 1080 (1080p) for each test function. + + Parameters: + session_page (Page): The page to reset. + """ session_page.goto("about:blank") # Reset screen size to 1080p session_page.set_viewport_size({"width": 1920, "height": 1080}) @@ -30,5 +49,11 @@ def page(session_page: Page) -> Page: @pytest.fixture(scope="module") def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, None]: + """ + Create a local Shiny app for testing. + + Parameters: + request (pytest.FixtureRequest): The request object for the fixture. + """ app_gen = local_app_fixture_gen(PurePath(request.path).parent / "app.py") yield next(app_gen) diff --git a/shiny/test/fixture/_fixture.py b/shiny/test/fixture/_fixture.py index 5fa357059..04fc72485 100644 --- a/shiny/test/fixture/_fixture.py +++ b/shiny/test/fixture/_fixture.py @@ -18,6 +18,14 @@ # Attempt up to 3 times to start the app, with a random port each time def local_app_fixture_gen(app: PurePath | str): + """ + Generate a local Shiny app fixture. + + Parameters + ---------- + app + The path to the Shiny app file. + """ has_yielded_app = False remaining_attempts = 3 @@ -53,10 +61,19 @@ def create_app_fixture( app: Union[PurePath, str], scope: ScopeName = "module", ): + """ + Create a fixture for a local Shiny app. + + Parameters + ---------- + app + The path to the Shiny app file. + scope + The scope of the fixture. + """ + @pytest.fixture(scope=scope) def fixture_func(): - # Pass through `yield` via `next(...)` call - # (`yield` must be on same line as `next`!) app_gen = local_app_fixture_gen(app) yield next(app_gen) diff --git a/tests/playwright/README.md b/tests/playwright/README.md index 5f4d3fa52..dd40199cb 100644 --- a/tests/playwright/README.md +++ b/tests/playwright/README.md @@ -68,7 +68,7 @@ use it from test funcs. import re from shiny.test import Page, ShinyAppProc -from shiny.test._internal import create_example_fixture +from conftest import create_example_fixture airmass_app = create_example_fixture("airmass") diff --git a/tests/playwright/shiny/inputs/input_file/test_input_file.py b/tests/playwright/shiny/inputs/input_file/test_input_file.py index 9fd5a93a1..0610773c2 100644 --- a/tests/playwright/shiny/inputs/input_file/test_input_file.py +++ b/tests/playwright/shiny/inputs/input_file/test_input_file.py @@ -2,7 +2,7 @@ from shiny.test import Page, ShinyAppProc, expect from shiny.test._controls import InputFile, OutputTable, OutputTextVerbatim -from shiny.test.playwright import FilePayload +from shiny.test._playwright import FilePayload def test_input_file_kitchen(page: Page, local_app: ShinyAppProc) -> None: From 23eb3c0b5c686cb1fe8362f3c6840f4a16a59897 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 14:20:56 -0400 Subject: [PATCH 06/14] Apply suggestions from code review --- shiny/test/fixture/_fixture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/test/fixture/_fixture.py b/shiny/test/fixture/_fixture.py index 04fc72485..d647d16db 100644 --- a/shiny/test/fixture/_fixture.py +++ b/shiny/test/fixture/_fixture.py @@ -62,7 +62,7 @@ def create_app_fixture( scope: ScopeName = "module", ): """ - Create a fixture for a local Shiny app. + Create a fixture for a local Shiny app directory. Parameters ---------- From b124d9fbb80699b3983535643f317f7ae09392c6 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 14:28:59 -0400 Subject: [PATCH 07/14] Do not expose `ScopeName` for now --- shiny/test/fixture/__init__.py | 3 +-- tests/playwright/conftest.py | 3 ++- tests/playwright/utils/deploy_utils.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/shiny/test/fixture/__init__.py b/shiny/test/fixture/__init__.py index 729cf084e..ea5f954ab 100644 --- a/shiny/test/fixture/__init__.py +++ b/shiny/test/fixture/__init__.py @@ -1,7 +1,6 @@ -from ._fixture import create_app_fixture, local_app_fixture_gen, ScopeName +from ._fixture import create_app_fixture, local_app_fixture_gen __all__ = ( "create_app_fixture", "local_app_fixture_gen", - "ScopeName", ) diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index 4dfc1897b..b7a99c367 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -5,7 +5,8 @@ from pathlib import PurePath -from shiny.test.fixture import ScopeName, create_app_fixture +from shiny.test.fixture import create_app_fixture +from shiny.test.fixture._fixture import ScopeName as ScopeName __all__ = ( "create_doc_example_fixture", diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index a40676e92..9c6b9e1cf 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -12,7 +12,8 @@ import pytest import requests -from shiny.test.fixture import ScopeName, local_app_fixture_gen +from shiny.test.fixture import local_app_fixture_gen +from conftest import ScopeName is_interactive = hasattr(sys, "ps1") reruns = 1 if is_interactive else 3 From 1c991ffd2aef7bd3b222dd654ccc7df61e3092df Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:07:31 -0400 Subject: [PATCH 08/14] Update docs. Make parameters snake case --- shiny/test/__init__.py | 9 +++---- shiny/test/_conftest.py | 52 ++++++++++++++++++++++++++++++----------- shiny/test/_expect.py | 13 +++++++---- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/shiny/test/__init__.py b/shiny/test/__init__.py index edf5ac33d..2ebfe86ca 100644 --- a/shiny/test/__init__.py +++ b/shiny/test/__init__.py @@ -8,20 +8,17 @@ ) -from ._conftest import ( - ShinyAppProc, - # run_shiny_app, -) +from ._conftest import ShinyAppProc # from ._expect import expect_to_change -from ._playwright import Page, expect, Locator +from ._playwright import Locator, Page, expect __all__ = ( - "expect", # TODO-future: Find the proper location for these methods to be returned # "run_shiny_app", # "expect_to_change", "ShinyAppProc", "Page", "Locator", + "expect", ) diff --git a/shiny/test/_conftest.py b/shiny/test/_conftest.py index 2e21d7148..325154a96 100644 --- a/shiny/test/_conftest.py +++ b/shiny/test/_conftest.py @@ -17,9 +17,11 @@ class OutputStream: - """Designed to wrap an IO[str] and accumulate the output using a bg thread + """ + Designed to wrap an IO[str] and accumulate the output using a bg thread - Also allows for blocking waits for particular lines.""" + Also allows for blocking waits for particular lines. + """ def __init__(self, io: IO[str], desc: Optional[str] = None): self._io = io @@ -33,7 +35,9 @@ def __init__(self, io: IO[str], desc: Optional[str] = None): self._thread.start() def _run(self): - """Pump lines into self._lines in a tight loop.""" + """ + Add lines into self._lines in a tight loop. + """ try: while not self._io.closed: @@ -53,7 +57,7 @@ def _run(self): self._closed = True self._cond.notify_all() - def wait_for(self, predicate: Callable[[str], bool], timeoutSecs: float) -> bool: + def wait_for(self, predicate: Callable[[str], bool], timeout_secs: float) -> bool: """ Wait until the predicate returns True for a line in the output. @@ -96,6 +100,24 @@ def dummyio() -> TextIO: class ShinyAppProc: + """ + Class that represents a running Shiny app process. + + This class is a context manager that can be used to run a Shiny app in a subprocess. It provides a way to interact + with the app and terminate it when it is no longer needed. + """ + + proc: subprocess.Popen[str] + """The subprocess object that represents the running Shiny app.""" + port: int + """The port that the Shiny app is running on.""" + url: str + """The URL that the Shiny app is running on.""" + stdout: OutputStream + """The standard output stream of the Shiny app subprocess.""" + stderr: OutputStream + """The standard error stream of the Shiny app subprocess.""" + def __init__(self, proc: subprocess.Popen[str], port: int): self.proc = proc self.port = port @@ -132,17 +154,21 @@ def __exit__( ): self.close() - def wait_until_ready(self, timeoutSecs: float) -> None: + def wait_until_ready(self, timeout_secs: float) -> None: """ Waits until the shiny app is ready to serve requests. - Parameters: - timeoutSecs (float): The maximum number of seconds to wait for the app to become ready. - - Raises: - ConnectionError: If there is an error while starting the shiny app. - TimeoutError: If the shiny app does not become ready within the specified timeout. - + Parameters + ---------- + timeout_secs + The maximum number of seconds to wait for the app to become ready. + + Raises + ------ + ConnectionError + If there is an error while starting the shiny app. + TimeoutError + If the shiny app does not become ready within the specified timeout. """ error_lines: List[str] = [] @@ -152,7 +178,7 @@ def stderr_uvicorn(line: str) -> bool: raise ConnectionError(f"Error while starting shiny app: `{line}`") return "Uvicorn running on" in line - if self.stderr.wait_for(stderr_uvicorn, timeoutSecs=timeoutSecs): + if self.stderr.wait_for(stderr_uvicorn, timeout_secs=timeout_secs): return else: raise TimeoutError( diff --git a/shiny/test/_expect.py b/shiny/test/_expect.py index 98e5d178c..a5800095d 100644 --- a/shiny/test/_expect.py +++ b/shiny/test/_expect.py @@ -8,7 +8,7 @@ @contextmanager def expect_to_change( - func: Callable[[], Any], timeoutSecs: float = 10 + func: Callable[[], Any], timeout_secs: float = 10 ) -> Generator[None, None, None]: """ Context manager that yields when the value returned by func() changes. Use this @@ -16,16 +16,19 @@ def expect_to_change( all browser actions), to prevent moving onto the next step of the test until this one has actually taken effect. - Raises TimeoutError if the value does not change within timeoutSecs. - Parameters ---------- func A function that returns a value. The value returned by this function is compared to the value returned by subsequent calls to this function. - timeoutSecs + timeout_secs How long to wait for the value to change before raising TimeoutError. + Raises + ------ + TimeoutError + If the value does not change within timeout_secs. + Example ------- @@ -37,7 +40,7 @@ def expect_to_change( original_value = func() yield - @retry_with_timeout(timeoutSecs) + @retry_with_timeout(timeout_secs) def wait_for_change(): if func() == original_value: raise AssertionError("Value did not change") From f948a8ab3e8539c4a7bf003ac4acdaa40768609c Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:08:46 -0400 Subject: [PATCH 09/14] Move shiny specific `page` (and `session_page`) fixtures out of `shiny.test` --- shiny/test/_pytest.py | 39 ----------------------------------- tests/playwright/conftest.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/shiny/test/_pytest.py b/shiny/test/_pytest.py index 8e134f2f0..a30537f79 100644 --- a/shiny/test/_pytest.py +++ b/shiny/test/_pytest.py @@ -6,45 +6,6 @@ import pytest from ._conftest import ShinyAppProc -from ._playwright import BrowserContext, Page -from .fixture import local_app_fixture_gen - - -# Make a single page fixture that can be used by all tests -@pytest.fixture(scope="session") -# By using a single page, the browser is only launched once and all tests run in the same tab / page. -def session_page(browser: BrowserContext) -> Page: - """ - Create a new page within the given browser context. - - Parameters: - browser (BrowserContext): The browser context in which to create the new page. - - Returns: - Page: The newly created page. - - """ - return browser.new_page() - - -@pytest.fixture(scope="function") -# By going to `about:blank`, we _reset_ the page to a known state before each test. -# It is not perfect, but it is faster than making a new page for each test. -# This must be done before each test -def page(session_page: Page) -> Page: - """ - Reset the given page to a known state before each test. - The page is built on the session_page, which is maintained over the full session. - The page will visit "about:blank" to reset between apps. - The default viewport size is set to 1920 x 1080 (1080p) for each test function. - - Parameters: - session_page (Page): The page to reset. - """ - session_page.goto("about:blank") - # Reset screen size to 1080p - session_page.set_viewport_size({"width": 1920, "height": 1080}) - return session_page @pytest.fixture(scope="module") diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index b7a99c367..14011f402 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -5,6 +5,9 @@ from pathlib import PurePath +import pytest + +from shiny.test._playwright import BrowserContext, Page from shiny.test.fixture import create_app_fixture from shiny.test.fixture._fixture import ScopeName as ScopeName @@ -20,6 +23,43 @@ here_root = here.parent.parent +# Make a single page fixture that can be used by all tests +@pytest.fixture(scope="session") +# By using a single page, the browser is only launched once and all tests run in the same tab / page. +def session_page(browser: BrowserContext) -> Page: + """ + Create a new page within the given browser context. + + Parameters: + browser (BrowserContext): The browser context in which to create the new page. + + Returns: + Page: The newly created page. + + """ + return browser.new_page() + + +@pytest.fixture(scope="function") +# By going to `about:blank`, we _reset_ the page to a known state before each test. +# It is not perfect, but it is faster than making a new page for each test. +# This must be done before each test +def page(session_page: Page) -> Page: + """ + Reset the given page to a known state before each test. + The page is built on the session_page, which is maintained over the full session. + The page will visit "about:blank" to reset between apps. + The default viewport size is set to 1920 x 1080 (1080p) for each test function. + + Parameters: + session_page (Page): The page to reset. + """ + session_page.goto("about:blank") + # Reset screen size to 1080p + session_page.set_viewport_size({"width": 1920, "height": 1080}) + return session_page + + def create_example_fixture( example_name: str, example_file: str = "app.py", From 260c35c100a49b63294860569daf0ee260712821 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:13:59 -0400 Subject: [PATCH 10/14] Rename `local_app_fixture_gen` to `shiny_app_gen` and pass through `run_shiny_app` parameters --- shiny/test/_conftest.py | 86 ++++++++++++++++++++++- shiny/test/_pytest.py | 6 +- shiny/test/fixture/__init__.py | 7 +- shiny/test/fixture/_fixture.py | 47 +------------ tests/playwright/examples/example_apps.py | 9 +-- 5 files changed, 96 insertions(+), 59 deletions(-) diff --git a/shiny/test/_conftest.py b/shiny/test/_conftest.py index 325154a96..37a1d9245 100644 --- a/shiny/test/_conftest.py +++ b/shiny/test/_conftest.py @@ -1,18 +1,20 @@ from __future__ import annotations import datetime +import logging import subprocess import sys import threading from pathlib import PurePath from types import TracebackType -from typing import IO, Callable, List, Optional, TextIO, Type, Union +from typing import IO, Any, Callable, Generator, List, Optional, TextIO, Type, Union import shiny._utils __all__ = ( "ShinyAppProc", "run_shiny_app", + "shiny_app_gen", ) @@ -70,7 +72,7 @@ def wait_for(self, predicate: Callable[[str], bool], timeout_secs: float) -> boo How long to wait for the predicate to return True before raising a TimeoutError. """ - timeoutAt = datetime.datetime.now() + datetime.timedelta(seconds=timeoutSecs) + timeout_at = datetime.datetime.now() + datetime.timedelta(seconds=timeout_secs) pos = 0 with self._cond: while True: @@ -81,7 +83,7 @@ def wait_for(self, predicate: Callable[[str], bool], timeout_secs: float) -> boo if self._closed: return False else: - remaining = (timeoutAt - datetime.datetime.now()).total_seconds() + remaining = (timeout_at - datetime.datetime.now()).total_seconds() if remaining < 0 or not self._cond.wait(timeout=remaining): # Timed out raise TimeoutError( @@ -240,3 +242,81 @@ def run_shiny_app( if wait_for_start: sa.wait_until_ready(timeout_secs) return sa + + +# Attempt up to 3 times to start the app, with a random port each time +def shiny_app_gen( + app_file: PurePath | str, + *, + start_attempts: int = 3, + port: int = 0, + cwd: Optional[str] = None, + # wait_for_start: bool = False, + timeout_secs: float = 10, + bufsize: int = 64 * 1024, +) -> Generator[ShinyAppProc, Any, None]: + """ + Run a Shiny app in a subprocess. + + This app will be automatically shut down when the Generator is exhausted. A + generator is returned so we can utilize the context manager methods of the + `ShinyAppProc` class (`__enter__` and `__exit__`). This allows for the app to be + automatically shut down when the context manager exists. (This exit method is not + possible when returning a ShinyAppProc directly.) + + Parameters + ---------- + app + The path to the Shiny app file. + start_attempts + Number of attempts to try and start the Shiny app. If the random port is already + in use, a new random port will be chosen and another attempt will be made. If + all attempts have been made, an error will be raised. + port + The port to run the app on. If 0, a random port will be chosen. + cwd + The working directory to run the app in. + timeout_secs + The maximum number of seconds to wait for the app to become ready. + bufsize + The buffer size to use for stdout and stderr. + + Yields + ------ + : + A single Shiny app process + """ + # wait_for_start + # If True, wait for the app to become ready before returning. + + has_yielded_app = False + while not has_yielded_app and start_attempts > 0: + start_attempts -= 1 + + # Make shiny process + sa = run_shiny_app( + app_file, + wait_for_start=False, + port=port, + cwd=cwd, + bufsize=bufsize, + timeout_secs=timeout_secs, + ) + try: + # enter / exit shiny context manager; (closes streams on exit) + with sa: + # Wait for shiny app to start + # Could throw a `ConnectionError` if the port is already in use + sa.wait_until_ready(30) + # Run app! + has_yielded_app = True + yield sa + except ConnectionError as e: + if start_attempts == 0: + # Ran out of attempts! + raise e + logging.error(f"Failed to bind to port: {e}") + # Try again with a new port! + finally: + if has_yielded_app: + logging.warning("Application output:\n" + str(sa.stderr)) diff --git a/shiny/test/_pytest.py b/shiny/test/_pytest.py index a30537f79..3acb3133e 100644 --- a/shiny/test/_pytest.py +++ b/shiny/test/_pytest.py @@ -5,7 +5,7 @@ import pytest -from ._conftest import ShinyAppProc +from ._conftest import ShinyAppProc, shiny_app_gen @pytest.fixture(scope="module") @@ -16,5 +16,5 @@ def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, N Parameters: request (pytest.FixtureRequest): The request object for the fixture. """ - app_gen = local_app_fixture_gen(PurePath(request.path).parent / "app.py") - yield next(app_gen) + sa_gen = shiny_app_gen(PurePath(request.path).parent / "app.py") + yield next(sa_gen) diff --git a/shiny/test/fixture/__init__.py b/shiny/test/fixture/__init__.py index ea5f954ab..686f4537a 100644 --- a/shiny/test/fixture/__init__.py +++ b/shiny/test/fixture/__init__.py @@ -1,6 +1,3 @@ -from ._fixture import create_app_fixture, local_app_fixture_gen +from ._fixture import create_app_fixture -__all__ = ( - "create_app_fixture", - "local_app_fixture_gen", -) +__all__ = ("create_app_fixture",) diff --git a/shiny/test/fixture/_fixture.py b/shiny/test/fixture/_fixture.py index d647d16db..6b1792ecb 100644 --- a/shiny/test/fixture/_fixture.py +++ b/shiny/test/fixture/_fixture.py @@ -1,59 +1,18 @@ from __future__ import annotations -import logging -import sys from pathlib import PurePath from typing import Literal, Union import pytest -from .._conftest import run_shiny_app +from .._conftest import shiny_app_gen __all__ = ( "create_app_fixture", - "local_app_fixture_gen", "ScopeName", ) -# Attempt up to 3 times to start the app, with a random port each time -def local_app_fixture_gen(app: PurePath | str): - """ - Generate a local Shiny app fixture. - - Parameters - ---------- - app - The path to the Shiny app file. - """ - - has_yielded_app = False - remaining_attempts = 3 - while not has_yielded_app and remaining_attempts > 0: - remaining_attempts -= 1 - - # Make shiny process - sa = run_shiny_app(app, wait_for_start=False, port=0) - try: - # enter / exit shiny context manager; (closes streams on exit) - with sa: - # Wait for shiny app to start - # Could throw a `ConnectionError` if the port is already in use - sa.wait_until_ready(30) - # Run app! - has_yielded_app = True - yield sa - except ConnectionError as e: - if remaining_attempts == 0: - # Ran out of attempts! - raise e - print(f"Failed to bind to port: {e}", file=sys.stderr) - # Try again with a new port! - finally: - if has_yielded_app: - logging.warning("Application output:\n" + str(sa.stderr)) - - ScopeName = Literal["session", "package", "module", "class", "function"] @@ -74,7 +33,7 @@ def create_app_fixture( @pytest.fixture(scope=scope) def fixture_func(): - app_gen = local_app_fixture_gen(app) - yield next(app_gen) + sa_gen = shiny_app_gen(app) + yield next(sa_gen) return fixture_func diff --git a/tests/playwright/examples/example_apps.py b/tests/playwright/examples/example_apps.py index 68f6dcffc..95118ee3d 100644 --- a/tests/playwright/examples/example_apps.py +++ b/tests/playwright/examples/example_apps.py @@ -6,6 +6,7 @@ from playwright.sync_api import ConsoleMessage, Page +from shiny.test import ShinyAppProc from shiny.test._conftest import run_shiny_app here_tests_e2e_examples = PurePath(__file__).parent @@ -155,7 +156,7 @@ def wait_for_idle_app( def validate_example(page: Page, ex_app_path: str) -> None: - app = run_shiny_app(pyshiny_root / ex_app_path, wait_for_start=True) + sa: ShinyAppProc = run_shiny_app(pyshiny_root / ex_app_path, wait_for_start=True) console_errors: typing.List[str] = [] @@ -169,8 +170,8 @@ def on_console_msg(msg: ConsoleMessage) -> None: page.on("console", on_console_msg) # Makes sure the app is closed when exiting the code block - with app: - page.goto(app.url) + with sa: + page.goto(sa.url) app_name = os.path.basename(os.path.dirname(ex_app_path)) short_app_path = f"{os.path.basename(os.path.dirname(os.path.dirname(ex_app_path)))}/{app_name}" @@ -188,7 +189,7 @@ def on_console_msg(msg: ConsoleMessage) -> None: ) # Check for py-shiny errors - error_lines = str(app.stderr).splitlines() + error_lines = str(sa.stderr).splitlines() # Remove any errors that are allowed error_lines = [ From 320bd69596ea4c835b08cd8f6b3db1894c619cd1 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:14:25 -0400 Subject: [PATCH 11/14] Update examples --- .../shiny/components/card-input/test_card-input.py | 8 +++++--- tests/playwright/utils/deploy_utils.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/playwright/shiny/components/card-input/test_card-input.py b/tests/playwright/shiny/components/card-input/test_card-input.py index 375dbbe9d..27466e1e1 100644 --- a/tests/playwright/shiny/components/card-input/test_card-input.py +++ b/tests/playwright/shiny/components/card-input/test_card-input.py @@ -2,7 +2,7 @@ import pytest -from shiny.test import Page +from shiny.test import Page, ShinyAppProc from shiny.test._conftest import run_shiny_app from shiny.test._controls import Card, OutputCode, ValueBox @@ -15,9 +15,11 @@ ], ) def test_card_input(page: Page, app_path: str, sel_card: str, sel_vb: str) -> None: - app = run_shiny_app(Path(__file__).parent / app_path) + sa: ShinyAppProc = run_shiny_app( + Path(__file__).parent / app_path, wait_for_start=True + ) - page.goto(app.url) + page.goto(sa.url) card = Card(page, sel_card) vb = ValueBox(page, sel_vb) diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index 9c6b9e1cf..84463ba74 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -11,10 +11,10 @@ import pytest import requests - -from shiny.test.fixture import local_app_fixture_gen from conftest import ScopeName +from shiny.test._conftest import shiny_app_gen + is_interactive = hasattr(sys, "ps1") reruns = 1 if is_interactive else 3 reruns_delay = 1 @@ -183,7 +183,7 @@ def fix_fn(request: pytest.FixtureRequest): deploy_location = request.param if deploy_location == LOCAL_LOCATION: - shinyapp_proc_gen = local_app_fixture_gen(app_file) + shinyapp_proc_gen = shiny_app_gen(app_file) # Return the `url` yield next(shinyapp_proc_gen).url elif deploy_location in deploy_locations: From 5e653b989edab483fc4dc1f8f0cb0f5ce90e9cbf Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:44:58 -0400 Subject: [PATCH 12/14] Add `.file: PurePath` to `ShinyAppProc` --- shiny/test/_conftest.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/shiny/test/_conftest.py b/shiny/test/_conftest.py index 37a1d9245..8543bb88f 100644 --- a/shiny/test/_conftest.py +++ b/shiny/test/_conftest.py @@ -109,6 +109,8 @@ class ShinyAppProc: with the app and terminate it when it is no longer needed. """ + file: PurePath + """The path to the Shiny app file.""" proc: subprocess.Popen[str] """The subprocess object that represents the running Shiny app.""" port: int @@ -120,7 +122,13 @@ class ShinyAppProc: stderr: OutputStream """The standard error stream of the Shiny app subprocess.""" - def __init__(self, proc: subprocess.Popen[str], port: int): + def __init__( + self, + proc: subprocess.Popen[str], + port: int, + *, + app_file: PurePath | str, + ): self.proc = proc self.port = port self.url = f"http://127.0.0.1:{port}/" @@ -128,6 +136,8 @@ def __init__(self, proc: subprocess.Popen[str], port: int): self.stderr = OutputStream(proc.stderr or dummyio()) threading.Thread(group=None, target=self._run, daemon=True).start() + self.file = PurePath(app_file) + def _run(self) -> None: self.proc.wait() if self.proc.stdout is not None: @@ -238,7 +248,8 @@ def run_shiny_app( # TODO: Detect early exit - sa = ShinyAppProc(child, shiny_port) + sa = ShinyAppProc(child, shiny_port, app_file=app_file) + if wait_for_start: sa.wait_until_ready(timeout_secs) return sa From 01444e5fa21b78fef94c1c6839fb742884215fdd Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:46:53 -0400 Subject: [PATCH 13/14] Move the retry logic from `shiny_app_gen` to `run_shiny_app` --- shiny/test/_conftest.py | 84 ++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/shiny/test/_conftest.py b/shiny/test/_conftest.py index 8543bb88f..7a80a5285 100644 --- a/shiny/test/_conftest.py +++ b/shiny/test/_conftest.py @@ -14,7 +14,8 @@ __all__ = ( "ShinyAppProc", "run_shiny_app", - "shiny_app_gen", + # For internal use only + # "shiny_app_gen", ) @@ -202,10 +203,11 @@ def stderr_uvicorn(line: str) -> bool: def run_shiny_app( app_file: Union[str, PurePath], *, + start_attempts: int = 3, port: int = 0, cwd: Optional[str] = None, wait_for_start: bool = True, - timeout_secs: float = 10, + timeout_secs: float = 30, bufsize: int = 64 * 1024, ) -> ShinyAppProc: """ @@ -251,10 +253,34 @@ def run_shiny_app( sa = ShinyAppProc(child, shiny_port, app_file=app_file) if wait_for_start: - sa.wait_until_ready(timeout_secs) + try: + sa.wait_until_ready(timeout_secs) + except ConnectionError as e: + logging.error(f"Failed to bind to port: {e}") + + # Make sure the current process is closed + sa.close() + + start_attempts -= 1 + if start_attempts < 1: + # Ran out of attempts! + raise e + + # Try again with a new port! + return run_shiny_app( + app_file, + start_attempts=start_attempts, + port=port, + cwd=cwd, + wait_for_start=wait_for_start, + timeout_secs=timeout_secs, + bufsize=bufsize, + ) + return sa +# Internal method to help make fixtures a little easier to write # Attempt up to 3 times to start the app, with a random port each time def shiny_app_gen( app_file: PurePath | str, @@ -263,7 +289,7 @@ def shiny_app_gen( port: int = 0, cwd: Optional[str] = None, # wait_for_start: bool = False, - timeout_secs: float = 10, + timeout_secs: float = 30, bufsize: int = 64 * 1024, ) -> Generator[ShinyAppProc, Any, None]: """ @@ -300,34 +326,22 @@ def shiny_app_gen( # wait_for_start # If True, wait for the app to become ready before returning. - has_yielded_app = False - while not has_yielded_app and start_attempts > 0: - start_attempts -= 1 - - # Make shiny process - sa = run_shiny_app( - app_file, - wait_for_start=False, - port=port, - cwd=cwd, - bufsize=bufsize, - timeout_secs=timeout_secs, - ) - try: - # enter / exit shiny context manager; (closes streams on exit) - with sa: - # Wait for shiny app to start - # Could throw a `ConnectionError` if the port is already in use - sa.wait_until_ready(30) - # Run app! - has_yielded_app = True - yield sa - except ConnectionError as e: - if start_attempts == 0: - # Ran out of attempts! - raise e - logging.error(f"Failed to bind to port: {e}") - # Try again with a new port! - finally: - if has_yielded_app: - logging.warning("Application output:\n" + str(sa.stderr)) + sa = run_shiny_app( + app_file, + wait_for_start=True, + start_attempts=start_attempts, + port=port, + cwd=cwd, + bufsize=bufsize, + timeout_secs=timeout_secs, + ) + had_connection_error: bool = False + try: + with sa: + yield sa + except ConnectionError as e: + had_connection_error = True + raise e + finally: + if not had_connection_error: + logging.warning("Application output:\n" + str(sa.stderr)) From 80e2063f927d6549fbe80b595d567d7d525523e6 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 30 May 2024 15:57:23 -0400 Subject: [PATCH 14/14] white space --- tests/playwright/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index 14011f402..08f17b40c 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -47,6 +47,7 @@ def session_page(browser: BrowserContext) -> Page: def page(session_page: Page) -> Page: """ Reset the given page to a known state before each test. + The page is built on the session_page, which is maintained over the full session. The page will visit "about:blank" to reset between apps. The default viewport size is set to 1920 x 1080 (1080p) for each test function.