Repository: posit-dev/py-shiny (language: python)

Issue:
Add testing API for py-shiny
Hi!
Right now, it's very hard to test the shiny part of your dashboard.
While the internals of py-shiny are tested with `playwright`, the testing API with `ShinyAppProc` and more is not in the package.

I'm keeping my fingers crossed for a quick release of `shiny.testing` namespace as it would allow us creating testable (i.e. enterprise ready) shiny apps in python, just like in R. :) 
Any ideas when to expect this feature?

Correct me if I'm wrong, but there's no issue on GH that tracks the progress of testing namespace, hence I created this one.

---

Relevant links:
- [Discord thread](https://discord.com/channels/1109483223987277844/1204870823257706586/1204942113574490173)
- [Discord `#testing` channel](https://discord.com/channels/1109483223987277844/1205562633793703996)
- [Example of simple ui test](https://github.com/posit-dev/py-shiny/blob/5ac456ac75859a513275a827f7e598ff8bd68e2f/tests/playwright/shiny/default-render-ui/test_default_render_ui.py)
- [Example of simple server test](https://github.com/posit-dev/py-shiny/blob/5ac456ac75859a513275a827f7e598ff8bd68e2f/tests/playwright/shiny/server/reactive_event/test_reactive_event.py)


Discussion / hints:
Hi @pstorozenko 👋 !

@karangattu and I are aiming for a testing release near conf in the early fall.  We are hoping to have the existing classes (and their methods) documented and moved under `shiny.test` (or something similar). 

---------------------------------

It seems reasonable to try to make basic testing possible in the near future (knowing the API might change), such as the [simple UI test linked above](https://github.com/posit-dev/py-shiny/blob/5ac456ac75859a513275a827f7e598ff8bd68e2f/tests/playwright/shiny/default-render-ui/test_default_render_ui.py). (Functions related to `conftest`)

The other helper `controls` could be aded piecemeal as we document / vet them.
Great to hear that!
In the meantime, I'm working on the blog post on how to test pyshiny apps with playwright right now, will share here and on discord once it's ready :)

**For all those wanting to test your apps right now**

It's great that there's an Epic for testing shiny apps 🎉 
In the meantime you can check [this repo with examples.](https://github.com/Appsilon/pyshiny-e2e-playwright)
[And the accompanying blog post](https://www.appsilon.com/post/testing-pyshiny-with-playwright).

For adding tests to the CI/CD pipeline you can use this snippet in `conftest.py` ([check this SO thread](https://stackoverflow.com/questions/34466027/what-is-conftest-py-for-in-pytest)).
This assumes the app is defined as `app = App(...)` in the `app.py` file.

```py
import threading

import pytest
import requests
from tenacity import retry, stop_after_delay, wait_fixed
from uvicorn import Config, Server

APP_HOST = "127.0.0.1"
APP_PORT = 8989
APP_URL = f"http://{APP_HOST}:{APP_PORT}"


@retry(wait=wait_fixed(0.5), stop=stop_after_delay(10))
def wait_for_server_to_start(url):
    response = requests.get(url)  # noqa: S113
    response.raise_for_status()  # Will raise an exception if the request is unsuccessful, i.e. server is not ready


def run_server():
    config = Config(app="app:app", host=APP_HOST, port=APP_PORT, log_level="info")
    server = Server(config)
    server.run()


@pytest.fixture(scope="session", autouse=True)
def _shiny_server():
    # Start the server in a background thread
    thread = threading.Thread(target=run_server, daemon=True)
    thread.start()

    wait_for_server_to_start(APP_URL)

    return
```

Fix patch:
diff --git a/setup.cfg b/setup.cfg
index 8a3f2ca01..f775280f8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -126,7 +126,7 @@ shiny = py.typed
 
 [options.entry_points]
 pytest11 =
-    shiny-test = shiny.test._pytest
+    shiny-test = shiny.pytest._pytest
 console_scripts =
     shiny = shiny._main:main
 
diff --git a/shiny/__init__.py b/shiny/__init__.py
index 85e75fc78..77571875e 100644
--- a/shiny/__init__.py
+++ b/shiny/__init__.py
@@ -1,6 +1,6 @@
 """A package for building reactive web applications."""
 
-__version__ = "0.10.2.9000"
+__version__ = "0.10.2.9001"
 
 from ._shinyenv import is_pyodide as _is_pyodide
 
diff --git a/shiny/playwright/__init__.py b/shiny/playwright/__init__.py
new file mode 100644
index 000000000..815c64b17
--- /dev/null
+++ b/shiny/playwright/__init__.py
@@ -0,0 +1,16 @@
+try:
+    import playwright  # noqa: F401 # pyright: ignore[reportUnusedImport, reportMissingTypeStubs]
+except ImportError:
+    raise ImportError(
+        "The shiny.playwright module requires the playwright package to be installed."
+        " Please install it with this command:"
+        "\n\n    pip install playwright"
+        "\n\n",
+        "If you are using pytest to test your code,"
+        " you can install the pytest-playwright shim package with this command:",
+        "\n\n    pip install pytest-playwright",
+    )
+
+from . import controls, expect
+
+__all__ = ["expect", "controls"]
diff --git a/shiny/playwright/_types.py b/shiny/playwright/_types.py
new file mode 100644
index 000000000..5c953ec88
--- /dev/null
+++ b/shiny/playwright/_types.py
@@ -0,0 +1,21 @@
+"""Facade classes for working with Shiny inputs/outputs in Playwright"""
+
+from __future__ import annotations
+
+import typing
+from typing import Optional
+
+OptionalStr = Optional[str]
+OptionalInt = Optional[int]
+OptionalFloat = Optional[float]
+OptionalBool = Optional[bool]
+
+PatternStr = typing.Pattern[str]
+PatternOrStr = typing.Union[str, PatternStr]
+ListPatternOrStr = typing.Union[
+    typing.List[PatternOrStr], typing.List[str], typing.List[PatternStr]
+]
+AttrValue = typing.Union[PatternOrStr, None]
+StyleValue = typing.Union[PatternOrStr, None]
+
+Timeout = typing.Union[float, None]
diff --git a/shiny/playwright/controls/__init__.py b/shiny/playwright/controls/__init__.py
new file mode 100644
index 000000000..6345cdd66
--- /dev/null
+++ b/shiny/playwright/controls/__init__.py
@@ -0,0 +1,95 @@
+from ._controls import (
+    Accordion,
+    Card,
+    DownloadButton,
+    DownloadLink,
+    InputActionButton,
+    InputActionLink,
+    InputCheckbox,
+    InputCheckboxGroup,
+    InputDarkMode,
+    InputDate,
+    InputDateRange,
+    InputFile,
+    InputNumeric,
+    InputPassword,
+    InputRadioButtons,
+    InputSelect,
+    InputSelectize,
+    InputSlider,
+    InputSliderRange,
+    InputSwitch,
+    InputTaskButton,
+    InputText,
+    InputTextArea,
+    NavItem,
+    NavsetBar,
+    NavsetCardPill,
+    NavsetCardTab,
+    NavsetCardUnderline,
+    NavsetHidden,
+    NavsetPill,
+    NavsetPillList,
+    NavsetTab,
+    NavsetUnderline,
+    OutputCode,
+    OutputDataFrame,
+    OutputImage,
+    OutputPlot,
+    OutputTable,
+    OutputText,
+    OutputTextVerbatim,
+    OutputUi,
+    Popover,
+    Sidebar,
+    Tooltip,
+    ValueBox,
+)
+
+__all__ = [
+    "InputActionButton",
+    "InputActionLink",
+    "InputCheckbox",
+    "InputCheckboxGroup",
+    "InputDarkMode",
+    "InputDate",
+    "InputDateRange",
+    "InputFile",
+    "InputNumeric",
+    "InputPassword",
+    "InputRadioButtons",
+    "InputSelect",
+    "InputSelectize",
+    "InputSlider",
+    "InputSliderRange",
+    "InputSwitch",
+    "InputTaskButton",
+    "InputText",
+    "InputTextArea",
+    "OutputCode",
+    "OutputDataFrame",
+    "OutputImage",
+    "OutputPlot",
+    "OutputTable",
+    "OutputText",
+    "OutputTextVerbatim",
+    "OutputUi",
+    "ValueBox",
+    "Card",
+    "Accordion",
+    "Sidebar",
+    "Popover",
+    "Tooltip",
+    "NavItem",
+    "NavsetBar",
+    "NavsetCardPill",
+    "NavsetCardTab",
+    "NavsetCardUnderline",
+    "NavsetHidden",
+    "NavsetPill",
+    "NavsetPillList",
+    "NavsetTab",
+    "NavsetUnderline",
+    "DownloadButton",
+    "DownloadLink",
+]
diff --git a/shiny/test/_controls.py b/shiny/playwright/controls/_controls.py
similarity index 93%
rename from shiny/test/_controls.py
rename to shiny/playwright/controls/_controls.py
index 3cad2913a..b7032ea85 100644
--- a/shiny/test/_controls.py
+++ b/shiny/playwright/controls/_controls.py
@@ -6,20 +6,32 @@
 import pathlib
 import platform
 import re
-import sys
 import time
 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 .._typing_extensions import TypeGuard  # pyright: ignore[reportPrivateImportUsage]
-from .._typing_extensions import (
-    assert_type,  # pyright: ignore[reportPrivateImportUsage]
+from ..._typing_extensions import TypeGuard, assert_type
+from ...types import MISSING, MISSING_TYPE
+from .._types import (
+    AttrValue,
+    ListPatternOrStr,
+    OptionalFloat,
+    PatternOrStr,
+    StyleValue,
+    Timeout,
+)
+from ..expect import (
+    expect_attribute_to_have_value,
+    expect_not_to_have_class,
+    expect_to_have_class,
+    expect_to_have_style,
 )
-from ..types import MISSING, MISSING_TYPE
-from ._playwright import FilePayload, FloatRect, Locator, Page, Position
-from ._playwright import expect as playwright_expect
+from ..expect._expect import _attr_match_str, _expect_class_value, _xpath_match_str
 
 """
 Questions:
@@ -61,21 +73,6 @@
   * If a method is used inconsistently, make/use a helper method
 """
 
-
-OptionalStr = Optional[str]
-OptionalInt = Optional[int]
-OptionalFloat = Optional[float]
-OptionalBool = Optional[bool]
-
-PatternStr = typing.Pattern[str]
-PatternOrStr = typing.Union[str, PatternStr]
-ListPatternOrStr = typing.Union[
-    typing.List[PatternOrStr], typing.List[str], typing.List[PatternStr]
-]
-AttrValue = typing.Union[PatternOrStr, None]
-StyleValue = typing.Union[PatternOrStr, None]
-
-Timeout = typing.Union[float, None]
 InitLocator = typing.Union[Locator, str]
 
 R = typing.TypeVar("R")
@@ -119,114 +116,6 @@ def set_text(
     loc.type(text, delay=delay, timeout=timeout)  # Type the value
 
 
-def expect_attr(
-    loc: Locator,
-    name: str,
-    value: AttrValue,
-    timeout: Timeout = None,
-) -> None:
-    """Expect an attribute to have a value. If `value` is `None`, then the attribute should not exist."""
-    if value is None:
-        # if isinstance(value, type(None)):
-        # Not allowed to have any value for the attribute
-        playwright_expect(loc).not_to_have_attribute(
-            name, re.compile(r".*"), timeout=timeout
-        )
-        return
-
-    playwright_expect(loc).to_have_attribute(name=name, value=value, timeout=timeout)
-
-
-def _expect_class_value(
-    loc: Locator,
-    cls: str,
-    has_class: bool,
-    timeout: Timeout = None,
-) -> None:
-    """Expect a locator to have (or not to have) a class value"""
-    if has_class:
-        expect_to_have_class(loc, cls, timeout=timeout)
-    else:
-        expect_not_to_have_class(loc, cls, timeout=timeout)
-
-
-def expect_to_have_class(
-    loc: Locator,
-    cls: str,
-    timeout: Timeout = None,
-) -> None:
-    """Expect a locator to contain a class value"""
-    cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)")
-    playwright_expect(loc).to_have_class(cls_regex, timeout=timeout)
-
-
-def expect_not_to_have_class(
-    loc: Locator,
-    cls: str,
-    timeout: Timeout = None,
-) -> None:
-    """Expect a locator not to contain a class value"""
-    cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)")
-    playwright_expect(loc).not_to_have_class(cls_regex, timeout=timeout)
-
-
-def _style_match_str(key: str, value: PatternOrStr) -> PatternStr:
-    if isinstance(value, str):
-        value_str = re.escape(value)
-    else:
-        value_str = value.pattern
-    return re.compile(rf"(^|;)\s*{re.escape(key)}\s*:\s*{value_str}\s*(;|$)")
-
-
-def _attr_match_str(key: str, value: str) -> str:
-    # Escape double quotes
-    value_str = value.replace('"', '\\"')
-    # `key` is `value`
-    return f'{key}="{value_str}"'
-    # assert_type(value, re.Pattern[str])
-    # # `key` contains `value`
-    # return f'{key}*="{value.pattern}"'
-
-
-def _xpath_match_str(key: str, value: PatternOrStr) -> str:
-    if isinstance(value, str):
-        # Escape double quotes
-        value_str = value.replace('"', '\\"')
-        # `key` is `value`
-        return f'@{key}="{value_str}"'
-    else:
-        # Disabling type assertion for earlier versions of Python
-        if sys.version_info >= (3, 10):
-            assert_type(value, re.Pattern[str])
-
-        # `key` contains `value`
-        return f'matches(@{key}, "{value.pattern}")'
-
-
-def expect_to_have_style(
-    loc: Locator,
-    css_key: str,
-    # Str representation for value. Will be put in a regex with `css_key`
-    css_value: StyleValue,
-    timeout: Timeout = None,
-) -> None:
-    """Expect the `style` attribute to have a value. If `value` is `None`, then the style attribute should not exist."""
-    if css_value is None:
-        # Not allowed to have any value for the style
-        playwright_expect(loc).not_to_have_attribute(
-            "style",
-            re.compile(rf"\b{re.escape(css_key)}\s*:"),
-            timeout=timeout,
-        )
-        return
-
-    playwright_expect(loc).to_have_attribute(
-        "style",
-        _style_match_str(css_key, css_value),
-        timeout=timeout,
-    )
-
-
 def _expect_multiple(loc: Locator, multiple: bool, timeout: Timeout = None) -> None:
     value = "True" if multiple else None
     expect_to_have_style(loc, "multiple", value, timeout=timeout)
@@ -391,7 +280,7 @@ def expect_width(
             The maximum time to wait for the expectation to be fulfilled.
             Defaults to None.
         """
-        expect_attr(self.loc, "width", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "width", value=value, timeout=timeout)
 
 
 class _WidthContainerM:
@@ -417,7 +306,9 @@ def expect_width(
             The maximum time to wait for the expectation to be fulfilled.
             Defaults to None.
         """
-        expect_attr(self.loc_container, "width", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc_container, "width", value=value, timeout=timeout
+        )
 
 
 class _SetTextM:
@@ -462,7 +353,7 @@ def expect_min(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "min", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "min", value=value, timeout=timeout)
 
     def expect_max(
         self,
@@ -470,7 +361,7 @@ def expect_max(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "max", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "max", value=value, timeout=timeout)
 
     def expect_step(
         self,
@@ -478,7 +369,7 @@ def expect_step(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "step", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "step", value=value, timeout=timeout)
 
 
 class _ExpectSpellcheckAttrM:
@@ -489,7 +380,9 @@ def expect_spellcheck(
         timeout: Timeout = None,
     ) -> None:
         # self.spellcheck.expect_to_have_value(value, timeout=timeout)
-        expect_attr(self.loc, "spellcheck", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "spellcheck", value=value, timeout=timeout
+        )
 
 
 class _ExpectPlaceholderAttrM:
@@ -499,7 +392,9 @@ def expect_placeholder(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "placeholder", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "placeholder", value=value, timeout=timeout
+        )
 
 
 class _ExpectAutocompleteAttrM:
@@ -509,7 +404,9 @@ def expect_autocomplete(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "autocomplete", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "autocomplete", value=value, timeout=timeout
+        )
 
 
 class InputText(
@@ -612,10 +509,10 @@ def expect_height(self, value: StyleValue, *, timeout: Timeout = None) -> None:
         expect_to_have_style(self.loc, "height", value, timeout=timeout)
 
     def expect_cols(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "cols", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "cols", value=value, timeout=timeout)
 
     def expect_rows(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "rows", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "rows", value=value, timeout=timeout)
 
     def expect_resize(
         self,
@@ -623,7 +520,7 @@ def expect_resize(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "resize", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "resize", value=value, timeout=timeout)
 
     def expect_autoresize(
         self,
@@ -762,7 +659,7 @@ def expect_multiple(self, multiple: bool, *, timeout: Timeout = None) -> None:
         _expect_multiple(self.loc, multiple, timeout=timeout)
 
     def expect_size(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(
+        expect_attribute_to_have_value(
             self.loc,
             "size",
             value=value,
@@ -860,18 +757,20 @@ def click(self, *, timeout: Timeout = None):
         return self
 
     def expect_mode(self, value: str, *, timeout: Timeout = None):
-        expect_attr(self.loc, "mode", value=value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "mode", value=value, timeout=timeout)
         self.expect_page_mode(value, timeout=timeout)
         return self
 
     def expect_page_mode(self, value: str, *, timeout: Timeout = None):
-        expect_attr(
+        expect_attribute_to_have_value(
             self.page.locator("html"), "data-bs-theme", value=value, timeout=timeout
         )
         return self
 
     def expect_wc_attribute(self, value: str, *, timeout: Timeout = None):
-        expect_attr(self.loc, "attribute", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "attribute", value=value, timeout=timeout
+        )
         return self
 
 
@@ -904,7 +803,7 @@ def __init__(
     def expect_state(
         self, value: Literal["ready", "busy"] | str, *, timeout: Timeout = None
     ):
-        expect_attr(
+        expect_attribute_to_have_value(
             self.loc.locator("> bslib-switch-inline"),
             name="case",
             value=value,
@@ -928,7 +827,7 @@ def expect_label_state(
         ).to_have_text(value, timeout=timeout)
 
     def expect_auto_reset(self, value: bool, timeout: Timeout = None):
-        expect_attr(
+        expect_attribute_to_have_value(
             self.loc,
             name="data-auto-reset",
             value="" if value else None,
@@ -1492,7 +1391,7 @@ def expect_accept(
     ) -> None:
         if isinstance(accept, list):
             accept = ",".join(accept)
-        expect_attr(self.loc, "accept", accept, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "accept", accept, timeout=timeout)
 
     def expect_width(self, width: StyleValue, *, timeout: Timeout = None) -> None:
         expect_to_have_style(self.loc_container, "width", width, timeout=timeout)
@@ -1511,10 +1410,12 @@ def expect_capture(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "capture", capture, timeout=timeout)
+        expect_attribute_to_have_value(self.loc, "capture", capture, timeout=timeout)
 
     def expect_placeholder(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc_file_display, "placeholder", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc_file_display, "placeholder", value=value, timeout=timeout
+        )
 
 
 class _InputSliderBase(_WidthLocM, _InputWithLabel):
@@ -1584,14 +1485,14 @@ def expect_animate_options(
         # TODO-future; Composable expectations
         self.expect_animate(exists=True, timeout=timeout)
         if not_is_missing(loop):
-            expect_attr(
+            expect_attribute_to_have_value(
                 self.loc_play_pause,
                 "data-loop",
                 "" if loop else None,
                 timeout=timeout,
             )
         if not_is_missing(interval):
-            expect_attr(
+            expect_attribute_to_have_value(
                 self.loc_play_pause,
                 "data-interval",
                 str(interval),
@@ -1618,25 +1519,39 @@ def click_pause(self, *, timeout: Timeout = None) -> None:
         self.loc_play_pause.click()
 
     def expect_min(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-min", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-min", value=value, timeout=timeout
+        )
 
     def expect_max(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-max", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-max", value=value, timeout=timeout
+        )
 
     def expect_step(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-step", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-step", value=value, timeout=timeout
+        )
 
     def expect_ticks(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-grid", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-grid", value=value, timeout=timeout
+        )
 
     def expect_sep(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-prettify-separator", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-prettify-separator", value=value, timeout=timeout
+        )
 
     def expect_pre(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-prefix", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-prefix", value=value, timeout=timeout
+        )
 
     def expect_post(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-postfix", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-postfix", value=value, timeout=timeout
+        )
 
     # def expect_data_type(
     #     self, value: AttrValue, *, timeout: Timeout = None
@@ -1644,13 +1559,19 @@ def expect_post(self, value: AttrValue, *, timeout: Timeout = None) -> None:
     #     expect_attr(self.loc, "data-data-type", value=value, timeout=timeout)
 
     def expect_time_format(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-time-format", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-time-format", value=value, timeout=timeout
+        )
 
     def expect_timezone(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-timezone", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-timezone", value=value, timeout=timeout
+        )
 
     def expect_drag_range(self, value: AttrValue, *, timeout: Timeout = None) -> None:
-        expect_attr(self.loc, "data-drag-interval", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-drag-interval", value=value, timeout=timeout
+        )
 
     def _wait_for_container(self, *, timeout: Timeout = None) -> None:
         self.loc_container.wait_for(state="visible", timeout=timeout)
@@ -1964,7 +1885,9 @@ def expect_min_date(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "data-min-date", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-min-date", value=value, timeout=timeout
+        )
 
     def expect_max_date(
         self,
@@ -1972,7 +1895,9 @@ def expect_max_date(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "data-max-date", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-max-date", value=value, timeout=timeout
+        )
 
     def expect_format(
         self,
@@ -1980,7 +1905,9 @@ def expect_format(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "data-date-format", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-date-format", value=value, timeout=timeout
+        )
 
     def expect_startview(
         self,
@@ -1988,7 +1915,9 @@ def expect_startview(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "data-date-start-view", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-date-start-view", value=value, timeout=timeout
+        )
 
     def expect_weekstart(
         self,
@@ -1998,7 +1927,9 @@ def expect_weekstart(
     ) -> None:
         if isinstance(value, int):
             value = str(value)
-        expect_attr(self.loc, "data-date-week-start", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-date-week-start", value=value, timeout=timeout
+        )
 
     def expect_language(
         self,
@@ -2006,7 +1937,9 @@ def expect_language(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "data-date-language", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-date-language", value=value, timeout=timeout
+        )
 
     # autoclose: bool = True,
     def expect_autoclose(
@@ -2015,7 +1948,9 @@ def expect_autoclose(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc, "data-date-autoclose", value=value, timeout=timeout)
+        expect_attribute_to_have_value(
+            self.loc, "data-date-autoclose", value=value, timeout=timeout
+        )
 
     def expect_datesdisabled(
         self,
@@ -2026,7 +1961,7 @@ def expect_datesdisabled(
         if isinstance(value, list):
             assert len(value) > 0, "`value` must be `None` or a non-empty list"
         value_str = "null" if value is None else json.dumps(value)
-        expect_attr(
+        expect_attribute_to_have_value(
             self.loc,
             "data-date-dates-disabled",
             value=value_str,
@@ -2042,7 +1977,7 @@ def expect_daysofweekdisabled(
         if isinstance(value, list):
             assert len(value) > 0, "`value` must be `None` or a non-empty list"
         value_str = "null" if value is None else json.dumps(value)
-        expect_attr(
+        expect_attribute_to_have_value(
             self.loc,
             "data-date-days-of-week-disabled",
             value=value_str,
@@ -2394,7 +2329,7 @@ def expect_img_src(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc_img, "src", value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc_img, "src", value, timeout=timeout)
 
     def expect_img_width(
         self,
@@ -2402,7 +2337,7 @@ def expect_img_width(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc_img, "width", value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc_img, "width", value, timeout=timeout)
 
     def expect_img_height(
         self,
@@ -2410,7 +2345,7 @@ def expect_img_height(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc_img, "height", value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc_img, "height", value, timeout=timeout)
 
     def expect_img_alt(
         self,
@@ -2418,7 +2353,7 @@ def expect_img_alt(
         *,
         timeout: Timeout = None,
     ) -> None:
-        expect_attr(self.loc_img, "alt", value, timeout=timeout)
+        expect_attribute_to_have_value(self.loc_img, "alt", value, timeout=timeout)
 
     # def expect_img_style(
     #     self,
@@ -3260,7 +3195,7 @@ def expect_body(self, value: PatternOrStr, *, timeout: Timeout = None) -> None:
 
     def expect_active(self, active: bool, *, timeout: Timeout = None) -> None:
         value = re.compile(r".*") if active else None
-        return expect_attr(
+        return expect_attribute_to_have_value(
             loc=self.loc_trigger,
             timeout=timeout,
             name="aria-describedby",
@@ -3268,7 +3203,7 @@ def expect_active(self, active: bool, *, timeout: Timeout = None) -> None:
         )
 
     def expect_placement(self, value: str, *, timeout: Timeout = None) -> None:
-        return expect_attr(
+        return expect_attribute_to_have_value(
             loc=self.get_loc_overlay_container(timeout=timeout),
             timeout=timeout,
             name="data-popper-placement",
@@ -3331,12 +3266,12 @@ def toggle(self, timeout: Timeout = None) -> None:
         self.loc_trigger.hover(timeout=timeout)
 
 
-class _LayoutNavItemBase(_InputWithContainer):
+class _NavItemBase(_InputWithContainer):
     def nav_item(
         self,
         value: str,
-    ) -> LayoutNavItem:
-        return LayoutNavItem(self.page, self.id, value)
+    ) -> NavItem:
+        return NavItem(self.page, self.id, value)
 
     def set(self, value: str, *, timeout: Timeout = None) -> None:
         self.nav_item(value).click(timeout=timeout)
@@ -3392,8 +3327,8 @@ def expect_nav_titles(
         self.expect.to_have_text(value, timeout=timeout)
 
 
-class LayoutNavItem(_InputWithContainer):
-    # *args: NavSetArg,
+class NavItem(_InputWithContainer):
+    # *args: NavsetArg,
     # id: Optional[str] = None,
     # selected: Optional[str] = None,
     # header: TagChild = None,
@@ -3428,8 +3363,8 @@ def expect_content(self, value: PatternOrStr, *, timeout: Timeout = None) -> Non
         playwright_expect(self.loc_content).to_have_text(value, timeout=timeout)
 
 
-class LayoutNavsetTab(_LayoutNavItemBase):
-    # *args: NavSetArg,
+class NavsetTab(_NavItemBase):
+    # *args: NavsetArg,
     # id: Optional[str] = None,
     # selected: Optional[str] = None,
     # header: TagChild = None,
@@ -3443,7 +3378,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetPill(_LayoutNavItemBase):
+class NavsetPill(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3453,7 +3388,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetUnderline(_LayoutNavItemBase):
+class NavsetUnderline(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3463,7 +3398,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetPillList(_LayoutNavItemBase):
+class NavsetPillList(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3473,7 +3408,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetCardTab(_LayoutNavItemBase):
+class NavsetCardTab(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3483,7 +3418,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetCardPill(_LayoutNavItemBase):
+class NavsetCardPill(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3493,7 +3428,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetCardUnderline(_LayoutNavItemBase):
+class NavsetCardUnderline(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3503,7 +3438,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetHidden(_LayoutNavItemBase):
+class NavsetHidden(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
@@ -3513,7 +3448,7 @@ def __init__(self, page: Page, id: str) -> None:
         )
 
 
-class LayoutNavSetBar(_LayoutNavItemBase):
+class NavsetBar(_NavItemBase):
     def __init__(self, page: Page, id: str) -> None:
         super().__init__(
             page,
diff --git a/shiny/playwright/expect/__init__.py b/shiny/playwright/expect/__init__.py
new file mode 100644
index 000000000..f61e6ac71
--- /dev/null
+++ b/shiny/playwright/expect/__init__.py
@@ -0,0 +1,15 @@
+from ._expect import (
+    expect_attribute_to_have_value,
+    expect_not_to_have_class,
+    expect_to_have_class,
+    expect_to_have_style,
+)
+from ._expect_to_change import expect_to_change
+
+__all__ = [
+    "expect_to_change",
+    "expect_attribute_to_have_value",
+    "expect_to_have_class",
+    "expect_not_to_have_class",
+    "expect_to_have_style",
+]
diff --git a/shiny/playwright/expect/_expect.py b/shiny/playwright/expect/_expect.py
new file mode 100644
index 000000000..89734bc94
--- /dev/null
+++ b/shiny/playwright/expect/_expect.py
@@ -0,0 +1,129 @@
+"""Facade classes for working with Shiny inputs/outputs in Playwright"""
+
+from __future__ import annotations
+
+import re
+import sys
+
+from playwright.sync_api import Locator
+from playwright.sync_api import expect as playwright_expect
+
+from ..._typing_extensions import assert_type
+from .._types import AttrValue, PatternOrStr, PatternStr, StyleValue, Timeout
+
+# Internal method only!
+# "_expect_class_value",
+__all__ = (
+    "expect_attribute_to_have_value",
+    "expect_to_have_class",
+    "expect_not_to_have_class",
+    "expect_to_have_style",
+)
+
+
+def expect_attribute_to_have_value(
+    loc: Locator,
+    name: str,
+    value: AttrValue,
+    timeout: Timeout = None,
+) -> None:
+    """Expect an attribute to have a value. If `value` is `None`, then the attribute should not exist."""
+    if value is None:
+        # if isinstance(value, type(None)):
+        # Not allowed to have any value for the attribute
+        playwright_expect(loc).not_to_have_attribute(
+            name, re.compile(r".*"), timeout=timeout
+        )
+        return
+
+    playwright_expect(loc).to_have_attribute(name=name, value=value, timeout=timeout)
+
+
+def expect_to_have_class(
+    loc: Locator,
+    cls: str,
+    timeout: Timeout = None,
+) -> None:
+    """Expect a locator to contain a class value"""
+    cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)")
+    playwright_expect(loc).to_have_class(cls_regex, timeout=timeout)
+
+
+def expect_not_to_have_class(
+    loc: Locator,
+    cls: str,
+    timeout: Timeout = None,
+) -> None:
+    """Expect a locator not to contain a class value"""
+    cls_regex = re.compile(rf"(^|\s+){re.escape(cls)}(\s+|$)")
+    playwright_expect(loc).not_to_have_class(cls_regex, timeout=timeout)
+
+
+def expect_to_have_style(
+    loc: Locator,
+    css_key: str,
+    # Str representation for value. Will be put in a regex with `css_key`
+    css_value: StyleValue,
+    timeout: Timeout = None,
+) -> None:
+    """Expect the `style` attribute to have a value. If `value` is `None`, then the style attribute should not exist."""
+    if css_value is None:
+        # Not allowed to have any value for the style
+        playwright_expect(loc).not_to_have_attribute(
+            "style",
+            re.compile(rf"\b{re.escape(css_key)}\s*:"),
+            timeout=timeout,
+        )
+        return
+
+    playwright_expect(loc).to_have_attribute(
+        "style",
+        _style_match_str(css_key, css_value),
+        timeout=timeout,
+    )
+
+
+def _expect_class_value(
+    loc: Locator,
+    cls: str,
+    has_class: bool,
+    timeout: Timeout = None,
+) -> None:
+    """Expect a locator to have (or not to have) a class value"""
+    if has_class:
+        expect_to_have_class(loc, cls, timeout=timeout)
+    else:
+        expect_not_to_have_class(loc, cls, timeout=timeout)
+
+
+def _attr_match_str(key: str, value: str) -> str:
+    # Escape double quotes
+    value_str = value.replace('"', '\\"')
+    # `key` is `value`
+    return f'{key}="{value_str}"'
+    # assert_type(value, re.Pattern[str])
+    # # `key` contains `value`
+    # return f'{key}*="{value.pattern}"'
+
+
+def _xpath_match_str(key: str, value: PatternOrStr) -> str:
+    if isinstance(value, str):
+        # Escape double quotes
+        value_str = value.replace('"', '\\"')
+        # `key` is `value`
+        return f'@{key}="{value_str}"'
+    else:
+        # Disabling type assertion for earlier versions of Python
+        if sys.version_info >= (3, 10):
+            assert_type(value, re.Pattern[str])
+
+        # `key` contains `value`
+        return f'matches(@{key}, "{value.pattern}")'
+
+
+def _style_match_str(key: str, value: PatternOrStr) -> PatternStr:
+    if isinstance(value, str):
+        value_str = re.escape(value)
+    else:
+        value_str = value.pattern
+    return re.compile(rf"(^|;)\s*{re.escape(key)}\s*:\s*{value_str}\s*(;|$)")
diff --git a/shiny/test/_expect.py b/shiny/playwright/expect/_expect_to_change.py
similarity index 100%
rename from shiny/test/_expect.py
rename to shiny/playwright/expect/_expect_to_change.py
diff --git a/shiny/run/__init__.py b/shiny/run/__init__.py
new file mode 100644
index 000000000..a3578f9c3
--- /dev/null
+++ b/shiny/run/__init__.py
@@ -0,0 +1,6 @@
+from ._run import ShinyAppProc, run_shiny_app
+
+__all__ = (
+    "run_shiny_app",
+    "ShinyAppProc",
+)
diff --git a/shiny/test/_conftest.py b/shiny/run/_run.py
similarity index 99%
rename from shiny/test/_conftest.py
rename to shiny/run/_run.py
index 7a80a5285..c07c6a532 100644
--- a/shiny/test/_conftest.py
+++ b/shiny/run/_run.py
@@ -9,7 +9,7 @@
 from types import TracebackType
 from typing import IO, Any, Callable, Generator, List, Optional, TextIO, Type, Union
 
-import shiny._utils
+from .._utils import random_port
 
 __all__ = (
     "ShinyAppProc",
@@ -228,7 +228,7 @@ def run_shiny_app(
     bufsize
         The buffer size to use for stdout and stderr.
     """
-    shiny_port = port if port != 0 else shiny._utils.random_port()
+    shiny_port = port if port != 0 else random_port()
 
     child = subprocess.Popen(
         [


Test patch:
diff --git a/shiny/pytest/__init__.py b/shiny/pytest/__init__.py
new file mode 100644
index 000000000..ee0cf91a5
--- /dev/null
+++ b/shiny/pytest/__init__.py
@@ -0,0 +1,12 @@
+try:
+    import pytest  # noqa: F401 # pyright: ignore[reportUnusedImport, reportMissingTypeStubs]
+except ImportError:
+    raise ImportError(
+        "The shiny.pytest module requires the pytest package to be installed."
+        " Please install it with this command:"
+        "\n\n    pip install pytest"
+    )
+
+from ._fixture import ScopeName, create_app_fixture
+
+__all__ = ("create_app_fixture", "ScopeName")
diff --git a/shiny/test/fixture/_fixture.py b/shiny/pytest/_fixture.py
similarity index 94%
rename from shiny/test/fixture/_fixture.py
rename to shiny/pytest/_fixture.py
index 6b1792ecb..9732fa53d 100644
--- a/shiny/test/fixture/_fixture.py
+++ b/shiny/pytest/_fixture.py
@@ -5,7 +5,7 @@
 
 import pytest
 
-from .._conftest import shiny_app_gen
+from ..run._run import shiny_app_gen
 
 __all__ = (
     "create_app_fixture",
diff --git a/shiny/test/_pytest.py b/shiny/pytest/_pytest.py
similarity index 87%
rename from shiny/test/_pytest.py
rename to shiny/pytest/_pytest.py
index 3acb3133e..57eea7a10 100644
--- a/shiny/test/_pytest.py
+++ b/shiny/pytest/_pytest.py
@@ -5,7 +5,8 @@
 
 import pytest
 
-from ._conftest import ShinyAppProc, shiny_app_gen
+from ..run import ShinyAppProc
+from ..run._run import shiny_app_gen
 
 
 @pytest.fixture(scope="module")
diff --git a/shiny/test/__init__.py b/shiny/test/__init__.py
deleted file mode 100644
index 2ebfe86ca..000000000
--- a/shiny/test/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-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 ._conftest import ShinyAppProc
-
-# from ._expect import expect_to_change
-from ._playwright import Locator, Page, expect
-
-__all__ = (
-    # 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/_playwright.py b/shiny/test/_playwright.py
deleted file mode 100644
index 06a561f70..000000000
--- a/shiny/test/_playwright.py
+++ /dev/null
@@ -1,17 +0,0 @@
-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/shiny/test/fixture/__init__.py b/shiny/test/fixture/__init__.py
deleted file mode 100644
index 686f4537a..000000000
--- a/shiny/test/fixture/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from ._fixture import create_app_fixture
-
-__all__ = ("create_app_fixture",)
diff --git a/tests/playwright/README.md b/tests/playwright/README.md
index dd40199cb..02280e609 100644
--- a/tests/playwright/README.md
+++ b/tests/playwright/README.md
@@ -49,7 +49,7 @@ as the calling `test_*.py` file.
 ```python
 
 import re
-from shiny.test import Page, ShinyAppProc, expect
+from playwright.sync_api import Locator, Page, expect ShinyAppProc, expect
 
 
 def test_airmass(page: Page, local_app: ShinyAppProc):
@@ -67,7 +67,7 @@ use it from test funcs.
 ```python
 import re
 
-from shiny.test import Page, ShinyAppProc
+from playwright.sync_api import Locator, Page, expect ShinyAppProc
 from conftest import create_example_fixture
 
 airmass_app = create_example_fixture("airmass")
diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py
index 08f17b40c..efe5b5516 100644
--- a/tests/playwright/conftest.py
+++ b/tests/playwright/conftest.py
@@ -6,10 +6,10 @@
 from pathlib import PurePath
 
 import pytest
+from playwright.sync_api import BrowserContext, Page
 
-from shiny.test._playwright import BrowserContext, Page
-from shiny.test.fixture import create_app_fixture
-from shiny.test.fixture._fixture import ScopeName as ScopeName
+from shiny.pytest import ScopeName as ScopeName
+from shiny.pytest import create_app_fixture
 
 __all__ = (
     "create_doc_example_fixture",
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 a8ac5c39e..175a2135d 100644
--- a/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py
+++ b/tests/playwright/deploys/express-accordion/test_deploys_express_accordion.py
@@ -1,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,7 @@
     skip_if_not_chrome,
 )
 
-from shiny.test import Page
-from shiny.test._controls import Accordion
+from shiny.playwright.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 d9107aab7..6db4a356f 100644
--- a/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py
+++ b/tests/playwright/deploys/express-dataframe/test_deploys_express_dataframe.py
@@ -1,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,7 @@
     skip_if_not_chrome,
 )
 
-from shiny.test import Page
-from shiny.test._controls import OutputDataFrame
+from shiny.playwright.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 49772c4f1..247fc0b89 100644
--- a/tests/playwright/deploys/express-folium/test_deploys_express_folium.py
+++ b/tests/playwright/deploys/express-folium/test_deploys_express_folium.py
@@ -1,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page, expect
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,6 @@
     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 b6bedb338..4ce83ba51 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,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page, expect
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,7 @@
     skip_if_not_chrome,
 )
 
-from shiny.test import Page, expect
-from shiny.test._controls import LayoutNavsetTab
+from shiny.playwright.controls import NavsetTab
 
 TIMEOUT = 2 * 60 * 1000
 
@@ -26,12 +26,12 @@ def test_page_default(page: Page, app_url: str) -> None:
 
     # Perform these tests second as their locators are not stable over time.
     # (They require that a locator be realized before finding the second locator)
-    nav_html = LayoutNavsetTab(page, "express_navset_tab")
+    nav_html = NavsetTab(page, "express_navset_tab")
     nav_html.expect_content("pre 0pre 1pre 2")
     nav_html.set("div")
     nav_html.expect_content("div 0\ndiv 1\ndiv 2")
     nav_html.set("span")
     nav_html.expect_content("span 0span 1span 2")
 
-    navset_card_tab = LayoutNavsetTab(page, "express_navset_card_tab")
+    navset_card_tab = NavsetTab(page, "express_navset_card_tab")
     navset_card_tab.expect_content("")
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 36874787e..cebd220f6 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,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,7 @@
     skip_if_not_chrome,
 )
 
-from shiny.test import Page
-from shiny.test._controls import Card, OutputTextVerbatim
+from shiny.playwright.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 a03157621..9735d40a1 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,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,7 @@
     skip_if_not_chrome,
 )
 
-from shiny.test import Page
-from shiny.test._controls import Card, OutputTextVerbatim
+from shiny.playwright.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 10cddd5b0..e4687916b 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,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,7 @@
     skip_if_not_chrome,
 )
 
-from shiny.test import Page
-from shiny.test._controls import OutputTextVerbatim, Sidebar
+from shiny.playwright.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 295cfdb82..d6b5d29fa 100644
--- a/tests/playwright/deploys/plotly/test_plotly_app.py
+++ b/tests/playwright/deploys/plotly/test_plotly_app.py
@@ -1,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page, expect
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,6 @@
     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 6429d5000..be697f413 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,4 +1,5 @@
 import pytest
+from playwright.sync_api import Page, expect
 from utils.deploy_utils import (
     create_deploys_app_url_fixture,
     reruns,
@@ -6,8 +7,6 @@
     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 95118ee3d..a3bf05c1a 100644
--- a/tests/playwright/examples/example_apps.py
+++ b/tests/playwright/examples/example_apps.py
@@ -6,8 +6,7 @@
 
 from playwright.sync_api import ConsoleMessage, Page
 
-from shiny.test import ShinyAppProc
-from shiny.test._conftest import run_shiny_app
+from shiny.run import ShinyAppProc, 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 a7bb2e205..475dda70a 100644
--- a/tests/playwright/examples/test_api_examples.py
+++ b/tests/playwright/examples/test_api_examples.py
@@ -1,7 +1,6 @@
 import pytest
 from example_apps import get_apps, reruns, reruns_delay, validate_example
-
-from shiny.test import Page
+from playwright.sync_api import Page
 
 
 @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay)
diff --git a/tests/playwright/examples/test_cpuinfo.py b/tests/playwright/examples/test_cpuinfo.py
index 4ab182321..e37c810a7 100644
--- a/tests/playwright/examples/test_cpuinfo.py
+++ b/tests/playwright/examples/test_cpuinfo.py
@@ -3,8 +3,9 @@
 import re
 
 from conftest import create_example_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
+from shiny.run import ShinyAppProc
 
 cpuinfo_app = create_example_fixture("cpuinfo")
 
diff --git a/tests/playwright/examples/test_examples.py b/tests/playwright/examples/test_examples.py
index 55ebf5f79..60117d7df 100644
--- a/tests/playwright/examples/test_examples.py
+++ b/tests/playwright/examples/test_examples.py
@@ -1,7 +1,6 @@
 import pytest
 from example_apps import get_apps, reruns, reruns_delay, validate_example
-
-from shiny.test import Page
+from playwright.sync_api 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 84aebdf92..83da63850 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 344105c78..4de95bfe8 100644
--- a/tests/playwright/shiny/_internal/test_e2e_regex_matching.py
+++ b/tests/playwright/shiny/_internal/test_e2e_regex_matching.py
@@ -1,6 +1,10 @@
 import re
 
-from shiny.test._controls import _attr_match_str, _style_match_str, _xpath_match_str
+from shiny.playwright.expect._expect 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 909bcaf9f..f81ebc673 100644
--- a/tests/playwright/shiny/async/test_async.py
+++ b/tests/playwright/shiny/async/test_async.py
@@ -1,8 +1,14 @@
 # See https://github.com/microsoft/playwright-python/issues/1532
 # pyright: reportUnknownMemberType=false
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputActionButton, InputTextArea, OutputTextVerbatim
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import (
+    InputActionButton,
+    InputTextArea,
+    OutputTextVerbatim,
+)
+from shiny.run import ShinyAppProc
 
 
 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 bd6b1b09f..a3fff9a92 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,8 +2,10 @@
 
 from typing import Optional
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputActionButton, InputSlider, OutputTextVerbatim
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import InputActionButton, InputSlider, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 6e29d09e7..b32b7490d 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,11 @@
 from __future__ import annotations
 
 from colors import bg_color, fg_color
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import Sidebar, _expect_class_value
+from shiny.playwright.controls import Sidebar
+from shiny.playwright.controls._controls import _expect_class_value
+from shiny.run import ShinyAppProc
 
 
 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 70f1b294a..97f0a6bf1 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,6 +1,8 @@
 from __future__ import annotations
 
-from shiny.test import Page, ShinyAppProc, expect
+from playwright.sync_api import Page, expect
+
+from shiny.run import ShinyAppProc
 
 
 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 e6f8bb53a..c8ec56bfa 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,9 @@
 
 import datetime
 
-from shiny.test import Page
-from shiny.test._controls import OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.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 39c6059a8..e1d8fc972 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
@@ -8,10 +8,10 @@
 import pytest
 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
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import (
+from shiny.playwright.controls import (
     DownloadButton,
     DownloadLink,
     InputActionButton,
@@ -36,6 +36,7 @@
     OutputTextVerbatim,
     OutputUi,
 )
+from shiny.run import ShinyAppProc
 
 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 822d72420..ba13320f3 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,7 +1,9 @@
 from __future__ import annotations
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputDataFrame, OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputDataFrame, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 5c2374c76..036e140b5 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,7 +1,13 @@
 from __future__ import annotations
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputActionButton, OutputDataFrame, OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import (
+    InputActionButton,
+    OutputDataFrame,
+    OutputTextVerbatim,
+)
+from shiny.run import ShinyAppProc
 
 
 def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
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 90f26fc70..92d934bc6 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
@@ -4,8 +4,8 @@
 
 from playwright.sync_api import Page, expect
 
-from shiny.test import ShinyAppProc
-from shiny.test._controls import OutputCode, OutputDataFrame
+from shiny.playwright.controls import OutputCode, OutputDataFrame
+from shiny.run import ShinyAppProc
 
 
 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 893bdcb03..f5895056d 100644
--- a/tests/playwright/shiny/components/accordion/test_accordion.py
+++ b/tests/playwright/shiny/components/accordion/test_accordion.py
@@ -1,8 +1,9 @@
 import pytest
 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
+from shiny.playwright.controls import Accordion, InputActionButton, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 @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 8a42d8b2d..12347fb03 100644
--- a/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py
+++ b/tests/playwright/shiny/components/busy_indicators/test_busy_indicators.py
@@ -1,8 +1,14 @@
 import os
 from urllib.parse import urlparse
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputRadioButtons, InputTaskButton, OutputTextVerbatim
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import (
+    InputRadioButtons,
+    InputTaskButton,
+    OutputTextVerbatim,
+)
+from shiny.run import ShinyAppProc
 
 
 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 27466e1e1..4130b6003 100644
--- a/tests/playwright/shiny/components/card-input/test_card-input.py
+++ b/tests/playwright/shiny/components/card-input/test_card-input.py
@@ -1,10 +1,10 @@
 from pathlib import Path
 
 import pytest
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._conftest import run_shiny_app
-from shiny.test._controls import Card, OutputCode, ValueBox
+from shiny.playwright.controls import Card, OutputCode, ValueBox
+from shiny.run import ShinyAppProc, run_shiny_app
 
 
 @pytest.mark.parametrize(
diff --git a/tests/playwright/shiny/components/data_frame/data_view_info/test_df_data_view_info.py b/tests/playwright/shiny/components/data_frame/data_view_info/test_df_data_view_info.py
index 95bc4cf7b..bcf07a58b 100644
--- a/tests/playwright/shiny/components/data_frame/data_view_info/test_df_data_view_info.py
+++ b/tests/playwright/shiny/components/data_frame/data_view_info/test_df_data_view_info.py
@@ -1,7 +1,7 @@
 from playwright.sync_api import Page
 
-from shiny.test import ShinyAppProc
-from shiny.test._controls import OutputDataFrame, OutputTextVerbatim
+from shiny.playwright.controls import OutputDataFrame, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None:
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 3c51171d4..cc2c4b32e 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
@@ -7,10 +7,11 @@
 import pytest
 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
+from shiny.playwright.controls import InputSelect, InputSwitch
+from shiny.playwright.expect import expect_to_change
+from shiny.run import ShinyAppProc
 
 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 67cbad517..fd4d070ef 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputActionButton, OutputCode, OutputDataFrame
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputActionButton, OutputCode, OutputDataFrame
+from shiny.run import ShinyAppProc
 
 
 def test_dataframe_organization_methods(page: Page, local_app: ShinyAppProc) -> None:
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 d193c7bf8..e3e9afe6f 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputDataFrame
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputDataFrame
+from shiny.run import ShinyAppProc
 
 
 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 c6834979d..7750c903e 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,5 +1,11 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputActionButton, OutputDataFrame, OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import (
+    InputActionButton,
+    OutputDataFrame,
+    OutputTextVerbatim,
+)
+from shiny.run import ShinyAppProc
 
 
 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 bf8b6ee40..b1020a04a 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputActionButton, OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputActionButton, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 b450fcd5d..fcd4a864f 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputDataFrame
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputDataFrame
+from shiny.run import ShinyAppProc
 
 
 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 013556d39..0b9e74bf5 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputDataFrame
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputDataFrame
+from shiny.run import ShinyAppProc
 
 
 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 652dda2e8..9489b1a6b 100644
--- a/tests/playwright/shiny/components/layout_columns/test_layout_columns.py
+++ b/tests/playwright/shiny/components/layout_columns/test_layout_columns.py
@@ -3,8 +3,9 @@
 from typing import TypeVar
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
+from shiny.run import 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 4e3a01598..1d4cdf241 100644
--- a/tests/playwright/shiny/components/nav/test_nav.py
+++ b/tests/playwright/shiny/components/nav/test_nav.py
@@ -2,17 +2,19 @@
 
 from dataclasses import dataclass
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import (
-    LayoutNavSetBar,
-    LayoutNavSetCardPill,
-    LayoutNavSetCardTab,
-    LayoutNavSetCardUnderline,
-    LayoutNavSetPill,
-    LayoutNavSetPillList,
-    LayoutNavsetTab,
-    LayoutNavSetUnderline,
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import (
+    NavsetBar,
+    NavsetCardPill,
+    NavsetCardTab,
+    NavsetCardUnderline,
+    NavsetPill,
+    NavsetPillList,
+    NavsetTab,
+    NavsetUnderline,
 )
+from shiny.run import ShinyAppProc
 
 
 def test_nav(page: Page, local_app: ShinyAppProc) -> None:
@@ -24,26 +26,26 @@ def test_nav(page: Page, local_app: ShinyAppProc) -> None:
     @dataclass
     class LayoutInfo:
         control: type[
-            LayoutNavSetBar
-            | LayoutNavSetCardPill
-            | LayoutNavSetCardTab
-            | LayoutNavSetCardUnderline
-            | LayoutNavSetPill
-            | LayoutNavSetPillList
-            | LayoutNavsetTab
-            | LayoutNavSetUnderline
+            NavsetBar
+            | NavsetCardPill
+            | NavsetCardTab
+            | NavsetCardUnderline
+            | NavsetPill
+            | NavsetPillList
+            | NavsetTab
+            | NavsetUnderline
         ]
         verify: str
 
     nav_data: list[LayoutInfo] = [
-        LayoutInfo(LayoutNavsetTab, "navset_tab()"),
-        LayoutInfo(LayoutNavSetPill, "navset_pill()"),
-        LayoutInfo(LayoutNavSetUnderline, "navset_underline()"),
-        LayoutInfo(LayoutNavSetCardTab, "navset_card_tab()"),
-        LayoutInfo(LayoutNavSetCardPill, "navset_card_pill()"),
-        LayoutInfo(LayoutNavSetCardUnderline, "navset_card_underline()"),
-        LayoutInfo(LayoutNavSetPillList, "navset_pill_list()"),
-        LayoutInfo(LayoutNavSetBar, "page_navbar()"),
+        LayoutInfo(NavsetTab, "navset_tab()"),
+        LayoutInfo(NavsetPill, "navset_pill()"),
+        LayoutInfo(NavsetUnderline, "navset_underline()"),
+        LayoutInfo(NavsetCardTab, "navset_card_tab()"),
+        LayoutInfo(NavsetCardPill, "navset_card_pill()"),
+        LayoutInfo(NavsetCardUnderline, "navset_card_underline()"),
+        LayoutInfo(NavsetPillList, "navset_pill_list()"),
+        LayoutInfo(NavsetBar, "page_navbar()"),
     ]
 
     for nav_info in nav_data:
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 4d7a05510..978a9ac0d 100644
--- a/tests/playwright/shiny/components/navset_hidden/test_nav_hidden.py
+++ b/tests/playwright/shiny/components/navset_hidden/test_nav_hidden.py
@@ -1,13 +1,15 @@
 # import pytest
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputRadioButtons, LayoutNavSetHidden
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputRadioButtons, NavsetHidden
+from shiny.run import ShinyAppProc
 
 
 def test_navset_hidden(page: Page, local_app: ShinyAppProc) -> None:
     page.goto(local_app.url)
 
     # navset_hidden
-    navset_hidden = LayoutNavSetHidden(page, "hidden_tabs")
+    navset_hidden = NavsetHidden(page, "hidden_tabs")
     input_radio_buttons = InputRadioButtons(page, "controller")
     navset_hidden.expect_nav_values(["panel1", "panel2", "panel3"])
     navset_hidden.expect_content("Panel 1 content")
diff --git a/tests/playwright/shiny/components/popover/test_popover.py b/tests/playwright/shiny/components/popover/test_popover.py
index 90115064e..ada43e906 100644
--- a/tests/playwright/shiny/components/popover/test_popover.py
+++ b/tests/playwright/shiny/components/popover/test_popover.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import Popover
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import Popover
+from shiny.run import ShinyAppProc
 
 
 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 de308f30c..d902d530d 100644
--- a/tests/playwright/shiny/components/test_sidebar.py
+++ b/tests/playwright/shiny/components/test_sidebar.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import OutputTextVerbatim, Sidebar
+from shiny.playwright.controls import OutputTextVerbatim, Sidebar
+from shiny.run import ShinyAppProc
 
 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 bf9dff4aa..c8f33bbf1 100644
--- a/tests/playwright/shiny/components/tooltip/test_tooltip.py
+++ b/tests/playwright/shiny/components/tooltip/test_tooltip.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import Tooltip
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import Tooltip
+from shiny.run import ShinyAppProc
 
 
 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 6947ddf0d..f5d72e24b 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,5 +1,8 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import ValueBox, expect_to_have_class
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import ValueBox
+from shiny.playwright.controls._controls import expect_to_have_class
+from shiny.run import ShinyAppProc
 
 
 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 cb4202986..badae5341 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,8 @@
 import pytest
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import ValueBox
+from shiny.playwright.controls import ValueBox
+from shiny.run import ShinyAppProc
 
 
 @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 68aec6da5..648b54446 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,4 +1,6 @@
-from shiny.test import Page, ShinyAppProc, expect
+from playwright.sync_api import Page, expect
+
+from shiny.run import ShinyAppProc
 
 
 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 e140dfa82..8f0a50874 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 6a7c24d8e..a16476c5e 100644
--- a/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py
+++ b/tests/playwright/shiny/experimental/card/kitchensink/test_card_ks.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import Card
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import Card
+from shiny.run import ShinyAppProc
 
 
 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 7a154a5ae..7f06948fc 100644
--- a/tests/playwright/shiny/experimental/card/test_card.py
+++ b/tests/playwright/shiny/experimental/card/test_card.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import Card
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import Card
+from shiny.run import ShinyAppProc
 
 
 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 6c1420163..3d4cd8907 100644
--- a/tests/playwright/shiny/implicit-register/test_implicit_register.py
+++ b/tests/playwright/shiny/implicit-register/test_implicit_register.py
@@ -1,4 +1,6 @@
-from shiny.test import Page, ShinyAppProc, expect
+from playwright.sync_api import Page, expect
+
+from shiny.run import ShinyAppProc
 
 
 def test_implicit_register(page: Page, local_app: ShinyAppProc) -> None:
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 0610773c2..5972d8899 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,9 @@
 from __future__ import annotations
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputFile, OutputTable, OutputTextVerbatim
-from shiny.test._playwright import FilePayload
+from playwright.sync_api import FilePayload, Page, expect
+
+from shiny.playwright.controls import InputFile, OutputTable, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 3431bb39d..c7e1a1226 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,7 +1,10 @@
 from __future__ import annotations
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputCheckboxGroup, InputRadioButtons, PatternOrStr
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import InputCheckboxGroup, InputRadioButtons
+from shiny.playwright.controls._controls import PatternOrStr
+from shiny.run import ShinyAppProc
 
 
 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 00bfa0325..1a0ba1a5d 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,8 +1,10 @@
 import re
 import time
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputSlider, InputSliderRange, OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputSlider, InputSliderRange, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 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 05942c387..807acd5ad 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,8 +2,10 @@
 
 import time
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputNumeric, InputTaskButton, OutputText
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputNumeric, InputTaskButton, OutputText
+from shiny.run import ShinyAppProc
 
 
 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 026c23e10..bc7348d1f 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,7 +1,9 @@
 from __future__ import annotations
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputTaskButton, OutputText
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputTaskButton, OutputText
+from shiny.run import ShinyAppProc
 
 
 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 f51058433..fdde05dd1 100644
--- a/tests/playwright/shiny/inputs/test_input_action_button_link.py
+++ b/tests/playwright/shiny/inputs/test_input_action_button_link.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputActionButton, InputActionLink
+from shiny.playwright.controls import InputActionButton, InputActionLink
+from shiny.run import ShinyAppProc
 
 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 3bf1795b1..f92bebb5b 100644
--- a/tests/playwright/shiny/inputs/test_input_checkbox.py
+++ b/tests/playwright/shiny/inputs/test_input_checkbox.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputCheckbox, OutputUi
+from shiny.playwright.controls import InputCheckbox, OutputUi
+from shiny.run import ShinyAppProc
 
 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 c1df2fbaf..ead8b4739 100644
--- a/tests/playwright/shiny/inputs/test_input_checkbox_group.py
+++ b/tests/playwright/shiny/inputs/test_input_checkbox_group.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputCheckboxGroup
+from shiny.playwright.controls import InputCheckboxGroup
+from shiny.run import ShinyAppProc
 
 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 556aaee4d..82301ea98 100644
--- a/tests/playwright/shiny/inputs/test_input_dark_mode.py
+++ b/tests/playwright/shiny/inputs/test_input_dark_mode.py
@@ -1,9 +1,10 @@
 from __future__ import annotations
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputActionButton, InputDarkMode, LayoutNavSetBar
+from shiny.playwright.controls import InputActionButton, InputDarkMode, NavsetBar
+from shiny.run import ShinyAppProc
 
 app = create_doc_example_core_fixture("input_dark_mode")
 
@@ -27,7 +28,7 @@ def test_input_dark_mode_switch(page: Page, app: ShinyAppProc) -> None:
     page.goto(app.url)
 
     mode_switch = InputDarkMode(page, "mode")
-    navbar = LayoutNavSetBar(page, "page")
+    navbar = NavsetBar(page, "page")
     make_light = InputActionButton(page, "make_light")
     make_dark = InputActionButton(page, "make_dark")
 
diff --git a/tests/playwright/shiny/inputs/test_input_date.py b/tests/playwright/shiny/inputs/test_input_date.py
index fbf7887c0..582d35317 100644
--- a/tests/playwright/shiny/inputs/test_input_date.py
+++ b/tests/playwright/shiny/inputs/test_input_date.py
@@ -5,9 +5,10 @@
 from typing import Literal
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputDate
+from shiny.playwright.controls import InputDate
+from shiny.run import ShinyAppProc
 
 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 dd704b224..b59b7fae7 100644
--- a/tests/playwright/shiny/inputs/test_input_date_range.py
+++ b/tests/playwright/shiny/inputs/test_input_date_range.py
@@ -5,9 +5,10 @@
 from typing import Literal
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputDateRange
+from shiny.playwright.controls import InputDateRange
+from shiny.run import ShinyAppProc
 
 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 2f7be1146..c44251acc 100644
--- a/tests/playwright/shiny/inputs/test_input_numeric.py
+++ b/tests/playwright/shiny/inputs/test_input_numeric.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputNumeric, OutputTextVerbatim
+from shiny.playwright.controls import InputNumeric, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 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 1d3f40103..4524ac249 100644
--- a/tests/playwright/shiny/inputs/test_input_password.py
+++ b/tests/playwright/shiny/inputs/test_input_password.py
@@ -1,7 +1,12 @@
 from conftest import create_doc_example_core_fixture
-
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputActionButton, InputPassword, OutputTextVerbatim
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import (
+    InputActionButton,
+    InputPassword,
+    OutputTextVerbatim,
+)
+from shiny.run import ShinyAppProc
 
 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 48199936b..6d94742eb 100644
--- a/tests/playwright/shiny/inputs/test_input_radio_buttons.py
+++ b/tests/playwright/shiny/inputs/test_input_radio_buttons.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputRadioButtons
+from shiny.playwright.controls import InputRadioButtons
+from shiny.run import ShinyAppProc
 
 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 9b3b67afa..16f810b6f 100644
--- a/tests/playwright/shiny/inputs/test_input_select.py
+++ b/tests/playwright/shiny/inputs/test_input_select.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputSelect
+from shiny.playwright.controls import InputSelect
+from shiny.run import ShinyAppProc
 
 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 546fa2e3e..35e7bdba5 100644
--- a/tests/playwright/shiny/inputs/test_input_selectize.py
+++ b/tests/playwright/shiny/inputs/test_input_selectize.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputSelectize
+from shiny.playwright.controls import InputSelectize
+from shiny.run import ShinyAppProc
 
 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 79fb9e9ec..f3d5b2b66 100644
--- a/tests/playwright/shiny/inputs/test_input_slider.py
+++ b/tests/playwright/shiny/inputs/test_input_slider.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputSlider, OutputTextVerbatim
+from shiny.playwright.controls import InputSlider, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 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 5b6bf823a..a00565609 100644
--- a/tests/playwright/shiny/inputs/test_input_switch.py
+++ b/tests/playwright/shiny/inputs/test_input_switch.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputSwitch, OutputUi
+from shiny.playwright.controls import InputSwitch, OutputUi
+from shiny.run import ShinyAppProc
 
 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 131c73c50..b23c2af42 100644
--- a/tests/playwright/shiny/inputs/test_input_text.py
+++ b/tests/playwright/shiny/inputs/test_input_text.py
@@ -1,9 +1,10 @@
 import re
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputText, OutputTextVerbatim
+from shiny.playwright.controls import InputText, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 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 c4531a79b..da331eb3a 100644
--- a/tests/playwright/shiny/inputs/test_input_text_area.py
+++ b/tests/playwright/shiny/inputs/test_input_text_area.py
@@ -1,9 +1,10 @@
 import re
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Locator, Page, expect
 
-from shiny.test import Locator, Page, ShinyAppProc, expect
-from shiny.test._controls import InputTextArea, OutputTextVerbatim
+from shiny.playwright.controls import InputTextArea, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 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 eb927b8f4..7ac72c3f5 100644
--- a/tests/playwright/shiny/inputs/test_inputs_update.py
+++ b/tests/playwright/shiny/inputs/test_inputs_update.py
@@ -1,10 +1,9 @@
 # pyright: reportUnknownMemberType=false
 
 from conftest import create_example_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import (
-    MISSING,
+from shiny.playwright.controls import (
     InputCheckbox,
     InputCheckboxGroup,
     InputDate,
@@ -16,6 +15,8 @@
     InputSliderRange,
     InputText,
 )
+from shiny.run import ShinyAppProc
+from shiny.types import MISSING
 
 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 967c386e3..bc0739087 100644
--- a/tests/playwright/shiny/module-conditional/test_module_conditional.py
+++ b/tests/playwright/shiny/module-conditional/test_module_conditional.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputCheckbox
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import InputCheckbox
+from shiny.run import ShinyAppProc
 
 
 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 6a08312d6..071352a8e 100644
--- a/tests/playwright/shiny/outputs/test_output_image.py
+++ b/tests/playwright/shiny/outputs/test_output_image.py
@@ -1,9 +1,10 @@
 import re
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputImage
+from shiny.playwright.controls import OutputImage
+from shiny.run import ShinyAppProc
 
 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 572b557db..67ae9664a 100644
--- a/tests/playwright/shiny/outputs/test_output_plot.py
+++ b/tests/playwright/shiny/outputs/test_output_plot.py
@@ -1,9 +1,10 @@
 import re
 
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputPlot
+from shiny.playwright.controls import OutputPlot
+from shiny.run import ShinyAppProc
 
 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 4f35af1de..3f494dcad 100644
--- a/tests/playwright/shiny/outputs/test_output_table.py
+++ b/tests/playwright/shiny/outputs/test_output_table.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputTable
+from shiny.playwright.controls import OutputTable
+from shiny.run import ShinyAppProc
 
 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 b47d281fb..c0a56ce5d 100644
--- a/tests/playwright/shiny/outputs/test_output_text.py
+++ b/tests/playwright/shiny/outputs/test_output_text.py
@@ -1,7 +1,8 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputText, OutputText, OutputTextVerbatim
+from shiny.playwright.controls import InputText, OutputText, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 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 80ce1be7a..97ae129b9 100644
--- a/tests/playwright/shiny/outputs/test_output_ui.py
+++ b/tests/playwright/shiny/outputs/test_output_ui.py
@@ -1,7 +1,13 @@
 from conftest import create_doc_example_core_fixture
-
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import InputActionButton, InputSlider, InputText, OutputUi
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import (
+    InputActionButton,
+    InputSlider,
+    InputText,
+    OutputUi,
+)
+from shiny.run import ShinyAppProc
 
 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 b1dd12625..cb9afc08b 100644
--- a/tests/playwright/shiny/plot-sizing/test_plot_sizing.py
+++ b/tests/playwright/shiny/plot-sizing/test_plot_sizing.py
@@ -1,7 +1,9 @@
 import re
 
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputPlot
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputPlot
+from shiny.run import ShinyAppProc
 
 
 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 fed2f9c0d..a8681ad0f 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,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 2996608e4..5bca87642 100644
--- a/tests/playwright/shiny/server/reactive_event/test_reactive_event.py
+++ b/tests/playwright/shiny/server/reactive_event/test_reactive_event.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import InputActionButton, OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import InputActionButton, OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 90dfd45e8..1b0bd7a1c 100644
--- a/tests/playwright/shiny/session/flush/test_on_flush.py
+++ b/tests/playwright/shiny/session/flush/test_on_flush.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc
-from shiny.test._controls import OutputTextVerbatim
+from playwright.sync_api import Page
+
+from shiny.playwright.controls import OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 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 82d6d8b6f..f58d20aab 100644
--- a/tests/playwright/shiny/shiny-express/hold/test_hold.py
+++ b/tests/playwright/shiny/shiny-express/hold/test_hold.py
@@ -1,5 +1,7 @@
-from shiny.test import Page, ShinyAppProc, expect
-from shiny.test._controls import OutputTextVerbatim
+from playwright.sync_api import Page, expect
+
+from shiny.playwright.controls import OutputTextVerbatim
+from shiny.run import ShinyAppProc
 
 
 def test_express_page_fluid(page: Page, local_app: ShinyAppProc) -> None:
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 feaba3dd7..c4e334f26 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,6 +1,7 @@
 from conftest import create_doc_example_core_fixture
+from playwright.sync_api import Page, expect
 
-from shiny.test import Page, ShinyAppProc, expect
+from shiny.run import ShinyAppProc
 
 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 84463ba74..38f40b1a3 100644
--- a/tests/playwright/utils/deploy_utils.py
+++ b/tests/playwright/utils/deploy_utils.py
@@ -13,7 +13,7 @@
 import requests
 from conftest import ScopeName
 
-from shiny.test._conftest import shiny_app_gen
+from shiny.run._run import shiny_app_gen
 
 is_interactive = hasattr(sys, "ps1")
 reruns = 1 if is_interactive else 3