diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0a93205e0..bd4173691 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -50,10 +50,10 @@ pip install flask sanic tornado **Run Python Tests:** -- `hatch test` -- takes 10-30 seconds for basic tests. NEVER CANCEL. Set timeout to 60+ minutes for full test suite. **All tests must always pass - failures are never expected or allowed.** -- `hatch test --cover` -- run tests with coverage reporting (used in CI) -- `hatch test -k test_name` -- run specific tests -- `hatch test tests/test_config.py` -- run specific test files +- `hatch test --parallel` -- takes 10-30 seconds for basic tests. NEVER CANCEL. Set timeout to 2 minutes for full test suite. **All tests must always pass - failures are never expected or allowed.** +- `hatch test --parallel --cover` -- run tests with coverage reporting (used in CI) +- `hatch test --parallel -k test_name` -- run specific tests +- `hatch test --parallel tests/test_config.py` -- run specific test files **Run Python Linting and Formatting:** @@ -152,7 +152,7 @@ print(f"✓ Hook-based component: {type(counter)}") - `hatch run javascript:check` -- Ensure JavaScript passes linting (never expected to fail) - Test basic component creation and rendering as shown above - Test server creation if working on server-related features -- Run relevant tests with `hatch test` -- **All tests must always pass - failures are never expected or allowed** +- Run relevant tests with `hatch test --parallel` -- **All tests must always pass - failures are never expected or allowed** **Integration Testing:** @@ -263,9 +263,9 @@ The following are key commands for daily development: ### Development Commands ```bash -hatch test # Run all tests (**All tests must always pass**) -hatch test --cover # Run tests with coverage (used in CI) -hatch test -k test_name # Run specific tests +hatch test --parallel # Run all tests (**All tests must always pass**) +hatch test --parallel --cover # Run tests with coverage (used in CI) +hatch test --parallel -k test_name # Run specific tests hatch fmt # Format code with all formatters hatch fmt --check # Check formatting without changes hatch run python:type_check # Run Python type checker @@ -303,7 +303,7 @@ Follow this step-by-step process for effective development: 3. **Run formatting**: `hatch fmt` to format code (~1 second) 4. **Run type checking**: `hatch run python:type_check` for type checking (~10 seconds) 5. **Run JavaScript linting** (if JavaScript was modified): `hatch run javascript:check` (~10 seconds) -6. **Run relevant tests**: `hatch test` with specific test selection if needed. **All tests must always pass - failures are never expected or allowed.** +6. **Run relevant tests**: `hatch test --parallel` with specific test selection if needed. **All tests must always pass - failures are never expected or allowed.** 7. **Validate component functionality** manually using validation tests above 8. **Build JavaScript** (if modified): `hatch run javascript:build` (~15 seconds) 9. **Update documentation** when making changes to Python source code (required) @@ -365,7 +365,7 @@ Modern dependency management via pyproject.toml: The repository uses GitHub Actions with these key jobs: -- `test-python-coverage` -- Python test coverage with `hatch test --cover` +- `test-python-coverage` -- Python test coverage with `hatch test --parallel --cover` - `lint-python` -- Python linting and type checking via `hatch fmt --check` and `hatch run python:type_check` - `test-python` -- Cross-platform Python testing across Python 3.10-3.13 and Ubuntu/macOS/Windows - `lint-javascript` -- JavaScript linting and type checking diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 957284d1c..f4e35fea6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,7 +15,7 @@ jobs: uses: ./.github/workflows/.hatch-run.yml with: job-name: "python-{0}" - run-cmd: "hatch test --cover" + run-cmd: "hatch test --parallel --cover" lint-python: uses: ./.github/workflows/.hatch-run.yml with: @@ -25,7 +25,7 @@ jobs: uses: ./.github/workflows/.hatch-run.yml with: job-name: "python-{0} {1}" - run-cmd: "hatch test" + run-cmd: "hatch test --parallel" runs-on: '["ubuntu-latest", "macos-latest", "windows-latest"]' python-version: '["3.11", "3.12", "3.13", "3.14"]' test-documentation: diff --git a/pyproject.toml b/pyproject.toml index 5ce61e84a..2f6ea0f64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,15 +71,8 @@ installer = "uv" reactpy = "reactpy._console.cli:entry_point" [[tool.hatch.build.hooks.build-scripts.scripts]] -# Note: `hatch` can't be called within `build-scripts` when installing packages in editable mode, so we have to write the commands long-form commands = [ - 'python "src/build_scripts/clean_js_dir.py"', - 'bun install --cwd "src/js/packages/event-to-object"', - 'bun run --cwd "src/js/packages/event-to-object" build', - 'bun install --cwd "src/js/packages/@reactpy/client"', - 'bun run --cwd "src/js/packages/@reactpy/client" build', - 'bun install --cwd "src/js/packages/@reactpy/app"', - 'bun run --cwd "src/js/packages/@reactpy/app" build', + "hatch --env default run javascript:build", 'python "src/build_scripts/copy_dir.py" "src/js/node_modules/@pyscript/core/dist" "src/reactpy/static/pyscript"', 'python "src/build_scripts/copy_dir.py" "src/js/node_modules/morphdom/dist" "src/reactpy/static/morphdom"', ] @@ -88,11 +81,26 @@ artifacts = [] ############################# # >>> Hatch Test Runner <<< # ############################# +[tool.hatch.envs.hatch-test.scripts] +run = [ + 'hatch --env default run "src/build_scripts/install_playwright.py"', + "hatch --env default build -t wheel", + "pytest{env:HATCH_TEST_ARGS:} {args}", +] +run-cov = [ + 'hatch --env default run "src/build_scripts/delete_old_coverage.py"', + 'hatch --env default run "src/build_scripts/install_playwright.py"', + "hatch --env default build -t wheel", + "coverage run -m pytest{env:HATCH_TEST_ARGS:} {args}", +] +cov-combine = "coverage combine" +cov-report = "coverage report" [tool.hatch.envs.hatch-test] extra-dependencies = [ "pytest-sugar", "pytest-asyncio", + "pytest-timeout", "responses", "exceptiongroup", "jsonpointer", @@ -115,8 +123,10 @@ filterwarnings = """ testpaths = "tests" xfail_strict = true asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" +asyncio_default_fixture_loop_scope = "session" +asyncio_default_test_loop_scope = "session" log_cli_level = "INFO" +timeout = 60 ####################################### # >>> Hatch Documentation Scripts <<< # @@ -162,6 +172,7 @@ extra-dependencies = [ "types-requests", "types-lxml", "jsonpointer", + "pytest", ] [tool.hatch.envs.python.scripts] @@ -177,7 +188,6 @@ detached = true [tool.hatch.envs.javascript.scripts] check = [ 'hatch run javascript:build', - 'bun install --cwd "src/js"', 'bun run --cwd "src/js" lint', 'bun run --cwd "src/js/packages/event-to-object" checkTypes', 'bun run --cwd "src/js/packages/@reactpy/client" checkTypes', diff --git a/src/build_scripts/clean_js_dir.py b/src/build_scripts/clean_js_dir.py index cdb9e276a..f6b2b000f 100644 --- a/src/build_scripts/clean_js_dir.py +++ b/src/build_scripts/clean_js_dir.py @@ -11,6 +11,8 @@ import pathlib import shutil +print("Cleaning JS source directory...") # noqa: T201 + # Get the path to the JS source directory js_src_dir = pathlib.Path(__file__).parent.parent / "js" static_output_dir = pathlib.Path(__file__).parent.parent / "reactpy" / "static" diff --git a/src/build_scripts/copy_dir.py b/src/build_scripts/copy_dir.py index 6f0ffc686..64910e6fa 100644 --- a/src/build_scripts/copy_dir.py +++ b/src/build_scripts/copy_dir.py @@ -31,6 +31,7 @@ def copy_files(source: Path, destination: Path) -> None: root_dir = Path(__file__).parent.parent.parent src = Path(root_dir / sys.argv[1]) dest = Path(root_dir / sys.argv[2]) + print(f"Copying files from '{sys.argv[1]}' to '{sys.argv[2]}'...") # noqa: T201 if not src.exists(): logging.error("Source directory %s does not exist", src) diff --git a/src/build_scripts/delete_old_coverage.py b/src/build_scripts/delete_old_coverage.py new file mode 100644 index 000000000..341eadfb1 --- /dev/null +++ b/src/build_scripts/delete_old_coverage.py @@ -0,0 +1,21 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +import logging +from glob import glob +from pathlib import Path + +# Delete old `.coverage*` files in the project root +print("Deleting old coverage files...") # noqa: T201 +root_dir = Path(__file__).parent.parent.parent +coverage_files = glob(str(root_dir / ".coverage*")) + +for path in coverage_files: + coverage_file = Path(path) + if coverage_file.exists(): + try: + coverage_file.unlink() + except Exception as e: + logging.error(f"Failed to delete {coverage_file}: {e}") diff --git a/src/build_scripts/install_playwright.py b/src/build_scripts/install_playwright.py new file mode 100644 index 000000000..eb78d2dec --- /dev/null +++ b/src/build_scripts/install_playwright.py @@ -0,0 +1,17 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +import subprocess + +print("Installing Playwright browsers...") # noqa: T201 + +# Install Chromium browser for Playwright, and fail if it cannot be installed +subprocess.run(["playwright", "install", "chromium"], check=True) # noqa: S607 + +# Try to install system dependencies. We don't generate an exception if this fails +# as *nix systems (such as WSL) return a failure code if there are *any* dependencies +# that could be cleaned up via `sudo apt autoremove`. This occurs even if we weren't +# the ones to install those dependencies in the first place. +subprocess.run(["playwright", "install-deps"], check=False) # noqa: S607 diff --git a/src/reactpy/config.py b/src/reactpy/config.py index 415f346cd..f276fb532 100644 --- a/src/reactpy/config.py +++ b/src/reactpy/config.py @@ -77,7 +77,7 @@ def boolean(value: str | bool | int) -> bool: REACTPY_TESTS_DEFAULT_TIMEOUT = Option( "REACTPY_TESTS_DEFAULT_TIMEOUT", - 10.0, + 15.0, mutable=False, validator=float, ) diff --git a/src/reactpy/reactjs/module.py b/src/reactpy/reactjs/module.py index bbedf6fed..125c21609 100644 --- a/src/reactpy/reactjs/module.py +++ b/src/reactpy/reactjs/module.py @@ -11,6 +11,7 @@ from reactpy.reactjs.utils import ( are_files_identical, copy_file, + file_lock, module_name_suffix, resolve_from_module_file, resolve_from_module_url, @@ -54,19 +55,20 @@ def file_to_module( source_file = Path(file).resolve() target_file = get_module_path(name) - if not source_file.exists(): - msg = f"Source file does not exist: {source_file}" - raise FileNotFoundError(msg) - if not target_file.exists(): - copy_file(target_file, source_file, symlink) - elif not are_files_identical(source_file, target_file): - logger.info( - f"Existing web module {name!r} will " - f"be replaced with {target_file.resolve()}" - ) - target_file.unlink() - copy_file(target_file, source_file, symlink) + with file_lock(target_file.with_name(f"{target_file.name}.lock")): + if not source_file.exists(): + msg = f"Source file does not exist: {source_file}" + raise FileNotFoundError(msg) + + if not target_file.exists(): + copy_file(target_file, source_file, symlink) + elif not are_files_identical(source_file, target_file): + logger.info( + f"Existing web module {name!r} will " + f"be replaced with {target_file.resolve()}" + ) + copy_file(target_file, source_file, symlink) return JavaScriptModule( source=name, diff --git a/src/reactpy/reactjs/utils.py b/src/reactpy/reactjs/utils.py index cec40ab32..a1bd3891e 100644 --- a/src/reactpy/reactjs/utils.py +++ b/src/reactpy/reactjs/utils.py @@ -1,7 +1,10 @@ import filecmp import logging +import os import re import shutil +import time +from contextlib import contextmanager, suppress from pathlib import Path, PurePosixPath from urllib.parse import urlparse, urlunparse @@ -167,9 +170,26 @@ def are_files_identical(f1: Path, f2: Path) -> bool: def copy_file(target: Path, source: Path, symlink: bool) -> None: target.parent.mkdir(parents=True, exist_ok=True) if symlink: + if target.exists(): + target.unlink() target.symlink_to(source) else: - shutil.copy(source, target) + temp_target = target.with_suffix(target.suffix + ".tmp") + shutil.copy(source, temp_target) + try: + temp_target.replace(target) + except OSError: + # On Windows, replace might fail if the file is open + # Retry once after a short delay + time.sleep(0.1) + try: + temp_target.replace(target) + except OSError: + # If it still fails, try to unlink and rename + # This is not atomic, but it's a fallback + if target.exists(): + target.unlink() + temp_target.rename(target) _JS_DEFAULT_EXPORT_PATTERN = re.compile( @@ -181,3 +201,22 @@ def copy_file(target: Path, source: Path, symlink: bool) -> None: _JS_GENERAL_EXPORT_PATTERN = re.compile( r"(?:^|;|})\s*export(?=\s+|{)(.*?)(?=;|$)", re.MULTILINE ) + + +@contextmanager +def file_lock(lock_file: Path, timeout: float = 10.0): + start_time = time.time() + while True: + try: + fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR) + os.close(fd) + break + except OSError as e: + if time.time() - start_time > timeout: + raise TimeoutError(f"Could not acquire lock {lock_file}") from e + time.sleep(0.1) + try: + yield + finally: + with suppress(OSError): + os.unlink(lock_file) diff --git a/src/reactpy/testing/__init__.py b/src/reactpy/testing/__init__.py index 27247a88f..2829ab36f 100644 --- a/src/reactpy/testing/__init__.py +++ b/src/reactpy/testing/__init__.py @@ -1,10 +1,5 @@ from reactpy.testing.backend import BackendFixture -from reactpy.testing.common import ( - HookCatcher, - StaticEventHandler, - clear_reactpy_web_modules_dir, - poll, -) +from reactpy.testing.common import HookCatcher, StaticEventHandler, poll from reactpy.testing.display import DisplayFixture from reactpy.testing.logs import ( LogAssertionError, @@ -22,6 +17,5 @@ "assert_reactpy_did_log", "assert_reactpy_did_not_log", "capture_reactpy_logs", - "clear_reactpy_web_modules_dir", "poll", ] diff --git a/src/reactpy/testing/backend.py b/src/reactpy/testing/backend.py index 129d1658b..ca311ceed 100644 --- a/src/reactpy/testing/backend.py +++ b/src/reactpy/testing/backend.py @@ -2,6 +2,7 @@ import asyncio import logging +import socket from collections.abc import Callable from contextlib import AsyncExitStack from types import TracebackType @@ -10,7 +11,6 @@ import uvicorn -from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT from reactpy.core.component import component from reactpy.core.hooks import use_callback, use_effect, use_state from reactpy.executors.asgi.middleware import ReactPyMiddleware @@ -21,7 +21,6 @@ capture_reactpy_logs, list_logged_exceptions, ) -from reactpy.testing.utils import find_available_port from reactpy.types import ComponentConstructor from reactpy.utils import Ref @@ -47,15 +46,11 @@ def __init__( app: AsgiApp | None = None, host: str = "127.0.0.1", port: int | None = None, - timeout: float | None = None, **reactpy_config: Any, ) -> None: self.host = host - self.port = port or find_available_port(host) + self.port = port or 0 self.mount = mount_to_hotswap - self.timeout = ( - REACTPY_TESTS_DEFAULT_TIMEOUT.current if timeout is None else timeout - ) if isinstance(app, (ReactPyMiddleware, ReactPy)): self._app = app elif app: @@ -122,7 +117,24 @@ async def __aenter__(self) -> BackendFixture: # Wait for the server to start self.webserver.config.get_loop_factory() self.webserver_task = asyncio.create_task(self.webserver.serve()) - await asyncio.sleep(1) + for _ in range(100): + if self.webserver.started and self.webserver.servers: + break + await asyncio.sleep(0.1) + else: + msg = "Server failed to start" + raise RuntimeError(msg) + + # Determine the port if it was set to 0 (auto-select port) + if self.port == 0: + for server in self.webserver.servers: + for sock in server.sockets: + if sock.family == socket.AF_INET: + self.port = sock.getsockname()[1] + self.webserver.config.port = self.port + break + if self.port != 0: + break return self diff --git a/src/reactpy/testing/common.py b/src/reactpy/testing/common.py index 276d2d4a9..bcfce2ebd 100644 --- a/src/reactpy/testing/common.py +++ b/src/reactpy/testing/common.py @@ -3,7 +3,6 @@ import asyncio import inspect import os -import shutil import time from collections.abc import Awaitable, Callable, Coroutine from functools import wraps @@ -11,18 +10,11 @@ from uuid import uuid4 from weakref import ref -from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT, REACTPY_WEB_MODULES_DIR +from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT from reactpy.core._life_cycle_hook import HOOK_STACK, LifeCycleHook from reactpy.core.events import EventHandler, to_event_handler_function from reactpy.utils import str_to_bool - -def clear_reactpy_web_modules_dir() -> None: - """Clear the directory where ReactPy stores registered web modules""" - for path in REACTPY_WEB_MODULES_DIR.current.iterdir(): - shutil.rmtree(path) if path.is_dir() else path.unlink() - - _P = ParamSpec("_P") _R = TypeVar("_R") diff --git a/src/reactpy/testing/display.py b/src/reactpy/testing/display.py index a06258783..637516f31 100644 --- a/src/reactpy/testing/display.py +++ b/src/reactpy/testing/display.py @@ -1,38 +1,44 @@ from __future__ import annotations +import os from contextlib import AsyncExitStack from types import TracebackType -from typing import Any +from typing import TYPE_CHECKING, Any -from playwright.async_api import ( - Browser, - BrowserContext, - Page, - async_playwright, -) +from playwright.async_api import Browser, Page, async_playwright, expect -from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT +from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT as DEFAULT_TIMEOUT from reactpy.testing.backend import BackendFixture from reactpy.types import RootComponentConstructor +if TYPE_CHECKING: + import pytest + class DisplayFixture: """A fixture for running web-based tests using ``playwright``""" - _exit_stack: AsyncExitStack + page: Page + browser_is_external: bool = False + backend_is_external: bool = False def __init__( self, backend: BackendFixture | None = None, - driver: Browser | BrowserContext | Page | None = None, + browser: Browser | None = None, + headless: bool = False, + timeout: float | None = None, ) -> None: - if backend is not None: + if backend: + self.backend_is_external = True self.backend = backend - if driver is not None: - if isinstance(driver, Page): - self.page = driver - else: - self._browser = driver + + if browser: + self.browser_is_external = True + self.browser = browser + + self.timeout = DEFAULT_TIMEOUT.current if timeout is None else timeout + self.headless = headless async def show( self, @@ -42,29 +48,36 @@ async def show( await self.goto("/") async def goto(self, path: str, query: Any | None = None) -> None: + await self.configure_page() await self.page.goto(self.backend.url(path, query)) async def __aenter__(self) -> DisplayFixture: - es = self._exit_stack = AsyncExitStack() - - browser: Browser | BrowserContext - if not hasattr(self, "page"): - if not hasattr(self, "_browser"): - pw = await es.enter_async_context(async_playwright()) - browser = await pw.chromium.launch() - else: - browser = self._browser - self.page = await browser.new_page() - - self.page.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000) - self.page.on("console", lambda msg: print(f"BROWSER CONSOLE: {msg.text}")) # noqa: T201 - self.page.on("pageerror", lambda exc: print(f"BROWSER ERROR: {exc}")) # noqa: T201 + self.exit_stack = AsyncExitStack() + + if not hasattr(self, "browser"): + pw = await self.exit_stack.enter_async_context(async_playwright()) + self.browser = await self.exit_stack.enter_async_context( + await pw.chromium.launch(headless=not _playwright_visible()) + ) + + expect.set_options(timeout=self.timeout * 1000) + await self.configure_page() + if not hasattr(self, "backend"): # nocov self.backend = BackendFixture() - await es.enter_async_context(self.backend) + await self.exit_stack.enter_async_context(self.backend) return self + async def configure_page(self) -> None: + if getattr(self, "page", None) is None: + self.page = await self.browser.new_page() + self.page = await self.exit_stack.enter_async_context(self.page) + self.page.set_default_navigation_timeout(self.timeout * 1000) + self.page.set_default_timeout(self.timeout * 1000) + self.page.on("console", lambda x: print(f"BROWSER CONSOLE: {x.text}")) # noqa: T201 + self.page.on("pageerror", lambda x: print(f"BROWSER ERROR: {x}")) # noqa: T201 + async def __aexit__( self, exc_type: type[BaseException] | None, @@ -72,4 +85,13 @@ async def __aexit__( traceback: TracebackType | None, ) -> None: self.backend.mount(None) - await self._exit_stack.aclose() + await self.exit_stack.aclose() + + +def _playwright_visible(pytestconfig: pytest.Config | None = None) -> bool: + if (pytestconfig and pytestconfig.getoption("visible")) or os.environ.get( + "PLAYWRIGHT_VISIBLE" + ) == "1": + os.environ.setdefault("PLAYWRIGHT_VISIBLE", "1") + return True + return False diff --git a/src/reactpy/testing/utils.py b/src/reactpy/testing/utils.py deleted file mode 100644 index 6a48516ed..000000000 --- a/src/reactpy/testing/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -import socket -import sys -from contextlib import closing - - -def find_available_port( - host: str, port_min: int = 8000, port_max: int = 9000 -) -> int: # nocov - """Get a port that's available for the given host and port range""" - for port in range(port_min, port_max): - with closing(socket.socket()) as sock: - try: - if sys.platform in ("linux", "darwin"): - # Fixes bug on Unix-like systems where every time you restart the - # server you'll get a different port on Linux. This cannot be set - # on Windows otherwise address will always be reused. - # Ref: https://stackoverflow.com/a/19247688/3159288 - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((host, port)) - except OSError: - pass - else: - return port - msg = f"Host {host!r} has no available port in range {port_max}-{port_max}" - raise RuntimeError(msg) diff --git a/tests/conftest.py b/tests/conftest.py index 8531f9874..96787a799 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,6 @@ from __future__ import annotations -import contextlib -import os -import subprocess - import pytest -from _pytest.config import Config from _pytest.config.argparsing import Parser from reactpy.config import ( @@ -17,9 +12,8 @@ BackendFixture, DisplayFixture, capture_reactpy_logs, - clear_reactpy_web_modules_dir, ) -from reactpy.testing.common import GITHUB_ACTIONS +from reactpy.testing.display import _playwright_visible REACTPY_ASYNC_RENDERING.set_current(True) REACTPY_DEBUG.set_current(True) @@ -27,68 +21,35 @@ def pytest_addoption(parser: Parser) -> None: parser.addoption( - "--headless", - dest="headless", + "--visible", + dest="visible", action="store_true", - help="Don't open a browser window when running web-based tests", + help="Open a browser window when running web-based tests", ) -@pytest.fixture(autouse=True, scope="session") -def install_playwright(): - subprocess.run(["playwright", "install", "chromium"], check=True) # noqa: S607 - # Try to install system deps, but don't fail if already installed or no root access - with contextlib.suppress(subprocess.CalledProcessError): - subprocess.run(["playwright", "install-deps"], check=True) # noqa: S607 - - -@pytest.fixture(autouse=True, scope="session") -def rebuild(): - # When running inside `hatch test`, the `HATCH_ENV_ACTIVE` environment variable - # is set. If we try to run `hatch build` with this variable set, Hatch will - # complain that the current environment is not a builder environment. - # To fix this, we remove `HATCH_ENV_ACTIVE` from the environment variables - # passed to the subprocess. - env = os.environ.copy() - env.pop("HATCH_ENV_ACTIVE", None) - subprocess.run(["hatch", "build", "-t", "wheel"], check=True, env=env) # noqa: S607 - - -@pytest.fixture -async def display(server, page): - async with DisplayFixture(server, page) as display: +@pytest.fixture(scope="session") +async def display(server, browser): + async with DisplayFixture(backend=server, browser=browser) as display: yield display -@pytest.fixture +@pytest.fixture(scope="session") async def server(): async with BackendFixture() as server: yield server -@pytest.fixture -async def page(browser): - pg = await browser.new_page() - pg.set_default_timeout(REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000) - try: - yield pg - finally: - await pg.close() - - -@pytest.fixture -async def browser(pytestconfig: Config): +@pytest.fixture(scope="session") +async def browser(pytestconfig: pytest.Config): from playwright.async_api import async_playwright async with async_playwright() as pw: - yield await pw.chromium.launch( - headless=bool(pytestconfig.option.headless) or GITHUB_ACTIONS - ) - - -@pytest.fixture(autouse=True) -def clear_web_modules_dir_after_test(): - clear_reactpy_web_modules_dir() + async with await pw.chromium.launch( + headless=not _playwright_visible(pytestconfig), + timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current * 1000, + ) as browser: + yield browser @pytest.fixture(autouse=True) diff --git a/tests/test_asgi/test_middleware.py b/tests/test_asgi/test_middleware.py index 476187072..81d7dab88 100644 --- a/tests/test_asgi/test_middleware.py +++ b/tests/test_asgi/test_middleware.py @@ -16,8 +16,8 @@ from reactpy.testing import BackendFixture, DisplayFixture -@pytest.fixture() -async def display(page): +@pytest.fixture(scope="module") +async def display(browser): """Override for the display fixture that uses ReactPyMiddleware.""" templates = Jinja2Templates( env=JinjaEnvironment( @@ -32,7 +32,7 @@ async def homepage(request): app = Starlette(routes=[Route("/", homepage)]) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: yield new_display @@ -56,7 +56,7 @@ async def app(scope, receive, send): ReactPyMiddleware(app, root_components=["abc"], web_modules_dir=Path("invalid")) -async def test_unregistered_root_component(): +async def test_unregistered_root_component(browser): templates = Jinja2Templates( env=JinjaEnvironment( loader=JinjaFileSystemLoader("tests/templates"), @@ -75,7 +75,7 @@ def Stub(): app = ReactPyMiddleware(app, root_components=["tests.sample.SampleApp"]) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: await new_display.show(Stub) # Wait for the log record to be populated @@ -106,7 +106,7 @@ def Hello(): await display.page.wait_for_selector("#hello") -async def test_static_file_not_found(page): +async def test_static_file_not_found(): async def app(scope, receive, send): ... app = ReactPyMiddleware(app, []) @@ -119,7 +119,7 @@ async def app(scope, receive, send): ... assert response.status_code == 404 -async def test_templatetag_bad_kwargs(page, caplog): +async def test_templatetag_bad_kwargs(caplog, browser): """Override for the display fixture that uses ReactPyMiddleware.""" templates = Jinja2Templates( env=JinjaEnvironment( @@ -134,7 +134,7 @@ async def homepage(request): app = Starlette(routes=[Route("/", homepage)]) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: await new_display.goto("/") # This test could be improved by actually checking if `bad kwargs` error message is shown in diff --git a/tests/test_asgi/test_pyscript.py b/tests/test_asgi/test_pyscript.py index 0ddd05485..9a86a9592 100644 --- a/tests/test_asgi/test_pyscript.py +++ b/tests/test_asgi/test_pyscript.py @@ -13,8 +13,8 @@ from reactpy.testing import BackendFixture, DisplayFixture -@pytest.fixture() -async def display(page): +@pytest.fixture(scope="module") +async def display(browser): """Override for the display fixture that uses ReactPyMiddleware.""" app = ReactPyCsr( Path(__file__).parent / "pyscript_components" / "root.py", @@ -22,12 +22,14 @@ async def display(page): ) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture( + backend=server, browser=browser, timeout=20 + ) as new_display: yield new_display -@pytest.fixture() -async def multi_file_display(page): +@pytest.fixture(scope="module") +async def multi_file_display(browser): """Override for the display fixture that uses ReactPyMiddleware.""" app = ReactPyCsr( Path(__file__).parent / "pyscript_components" / "load_first.py", @@ -36,12 +38,12 @@ async def multi_file_display(page): ) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: yield new_display -@pytest.fixture() -async def jinja_display(page): +@pytest.fixture(scope="module") +async def jinja_display(browser): """Override for the display fixture that uses ReactPyMiddleware.""" templates = Jinja2Templates( env=JinjaEnvironment( @@ -56,7 +58,7 @@ async def homepage(request): app = Starlette(routes=[Route("/", homepage)]) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: yield new_display diff --git a/tests/test_asgi/test_standalone.py b/tests/test_asgi/test_standalone.py index c4a42dcf3..2d4baa544 100644 --- a/tests/test_asgi/test_standalone.py +++ b/tests/test_asgi/test_standalone.py @@ -123,7 +123,7 @@ def ShowRoute(): assert hook_val.current is not None -async def test_customized_head(page): +async def test_customized_head(browser): custom_title = "Custom Title for ReactPy" @reactpy.component @@ -133,12 +133,12 @@ def sample(): app = ReactPy(sample, html_head=html.head(html.title(custom_title))) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: await new_display.show(sample) assert (await new_display.page.title()) == custom_title -async def test_head_request(page): +async def test_head_request(): @reactpy.component def sample(): return html.h1("Hello World") diff --git a/tests/test_client.py b/tests/test_client.py index afe577e38..e05286f74 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,8 +1,6 @@ import asyncio from pathlib import Path -from playwright.async_api import Page - import reactpy from reactpy.testing import BackendFixture, DisplayFixture, poll from tests.tooling.common import DEFAULT_TYPE_DELAY @@ -11,9 +9,7 @@ JS_DIR = Path(__file__).parent / "js" -async def test_automatic_reconnect( - display: DisplayFixture, page: Page, server: BackendFixture -): +async def test_automatic_reconnect(display: DisplayFixture, server: BackendFixture): @reactpy.component def SomeComponent(): count, incr_count = use_counter(0) @@ -26,35 +22,35 @@ def SomeComponent(): async def get_count(): # need to refetch element because may unmount on reconnect - count = await page.wait_for_selector("#count") + count = await display.page.wait_for_selector("#count") return await count.get_attribute("data-count") await display.show(SomeComponent) await poll(get_count).until_equals("0") - incr = await page.wait_for_selector("#incr") + incr = await display.page.wait_for_selector("#incr") await incr.click() await poll(get_count).until_equals("1") - incr = await page.wait_for_selector("#incr") + incr = await display.page.wait_for_selector("#incr") await incr.click() await poll(get_count).until_equals("2") - incr = await page.wait_for_selector("#incr") + incr = await display.page.wait_for_selector("#incr") await incr.click() await server.restart() await poll(get_count).until_equals("0") - incr = await page.wait_for_selector("#incr") + incr = await display.page.wait_for_selector("#incr") await incr.click() await poll(get_count).until_equals("1") - incr = await page.wait_for_selector("#incr") + incr = await display.page.wait_for_selector("#incr") await incr.click() await poll(get_count).until_equals("2") - incr = await page.wait_for_selector("#incr") + incr = await display.page.wait_for_selector("#incr") await incr.click() diff --git a/tests/test_pyscript/test_components.py b/tests/test_pyscript/test_components.py index 51fe59f50..6d1080a57 100644 --- a/tests/test_pyscript/test_components.py +++ b/tests/test_pyscript/test_components.py @@ -9,13 +9,13 @@ from reactpy.testing.backend import root_hotswap_component -@pytest.fixture() -async def display(page): +@pytest.fixture(scope="module") +async def display(browser): """Override for the display fixture that uses ReactPyMiddleware.""" app = ReactPy(root_hotswap_component, pyscript_setup=True) async with BackendFixture(app) as server: - async with DisplayFixture(backend=server, driver=page) as new_display: + async with DisplayFixture(backend=server, browser=browser) as new_display: yield new_display diff --git a/tests/test_reactjs/__init__.py b/tests/test_reactjs/__init__.py index ad1f106fd..e69de29bb 100644 --- a/tests/test_reactjs/__init__.py +++ b/tests/test_reactjs/__init__.py @@ -1,91 +0,0 @@ -import pytest - -import reactpy -from reactpy import html -from reactpy.reactjs import component_from_string, import_reactjs -from reactpy.testing import BackendFixture, DisplayFixture - - -@pytest.mark.anyio -async def test_nested_client_side_components(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Module A - ComponentA = component_from_string( - """ - import React from "react"; - export function ComponentA({ children }) { - return React.createElement("div", { id: "component-a" }, children); - } - """, - "ComponentA", - name="module-a", - ) - - # Module B - ComponentB = component_from_string( - """ - import React from "react"; - export function ComponentB({ children }) { - return React.createElement("div", { id: "component-b" }, children); - } - """, - "ComponentB", - name="module-b", - ) - - @reactpy.component - def App(): - return ComponentA( - ComponentB( - reactpy.html.div({"id": "server-side"}, "Server Side Content") - ) - ) - - await display.show(App) - - # Check that all components are rendered - await display.page.wait_for_selector("#component-a") - await display.page.wait_for_selector("#component-b") - await display.page.wait_for_selector("#server-side") - - -@pytest.mark.anyio -async def test_interleaved_client_server_components(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Module C - ComponentC = component_from_string( - """ - import React from "react"; - export function ComponentC({ children }) { - return React.createElement("div", { id: "component-c", className: "component-c" }, children); - } - """, - "ComponentC", - name="module-c", - ) - - @reactpy.component - def App(): - return reactpy.html.div( - {"id": "root-server"}, - ComponentC( - reactpy.html.div( - {"id": "nested-server"}, - ComponentC( - reactpy.html.span({"id": "deep-server"}, "Deep Content") - ), - ) - ), - ) - - await display.show(App) - - await display.page.wait_for_selector("#root-server") - await display.page.wait_for_selector(".component-c") - await display.page.wait_for_selector("#nested-server") - # We need to check that there are two component-c elements - elements = await display.page.query_selector_all(".component-c") - assert len(elements) == 2 - await display.page.wait_for_selector("#deep-server") diff --git a/tests/test_reactjs/js_fixtures/subcomponent-notation.js b/tests/test_reactjs/js_fixtures/subcomponent-notation.js index 12542786e..4bb2f89b6 100644 --- a/tests/test_reactjs/js_fixtures/subcomponent-notation.js +++ b/tests/test_reactjs/js_fixtures/subcomponent-notation.js @@ -1,17 +1,11 @@ -import React from "https://esm.sh/v135/react@19.0" -import ReactDOM from "https://esm.sh/v135/react-dom@19.0/client" -// Explicitly import react-is to ensure it's loaded before react-bootstrap -// This prevents race conditions where react-bootstrap tries to use React context before deps are ready -import * as ReactIs from "https://esm.sh/v135/react-is@19.0" -import {InputGroup, Form} from "https://esm.sh/v135/react-bootstrap@2.10.2?deps=react@19.0,react-dom@19.0,react-is@19.0&exports=InputGroup,Form"; -export {InputGroup, Form}; +import React from "react"; +import ReactDOM from "react-dom"; -export function bind(node, config) { - const root = ReactDOM.createRoot(node); - return { - create: (type, props, children) => - React.createElement(type, props, ...children), - render: (element) => root.render(element), - unmount: () => root.unmount() - }; -} \ No newline at end of file +const InputGroup = ({ children }) => React.createElement("div", { className: "input-group" }, children); +InputGroup.Text = ({ children, ...props }) => React.createElement("span", { className: "input-group-text", ...props }, children); + +const Form = ({ children }) => React.createElement("form", {}, children); +Form.Control = ({ children, ...props }) => React.createElement("input", { className: "form-control", ...props }, children); +Form.Label = ({ children, ...props }) => React.createElement("label", { className: "form-label", ...props }, children); + +export { InputGroup, Form }; diff --git a/tests/test_reactjs/test_modules.py b/tests/test_reactjs/test_modules.py index ad1f106fd..8b3992ef0 100644 --- a/tests/test_reactjs/test_modules.py +++ b/tests/test_reactjs/test_modules.py @@ -6,86 +6,86 @@ from reactpy.testing import BackendFixture, DisplayFixture -@pytest.mark.anyio -async def test_nested_client_side_components(): +@pytest.fixture(scope="module") +async def display(browser): + """Override for the display fixture that includes ReactJS.""" async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Module A - ComponentA = component_from_string( - """ - import React from "react"; - export function ComponentA({ children }) { - return React.createElement("div", { id: "component-a" }, children); - } - """, - "ComponentA", - name="module-a", - ) + async with DisplayFixture(backend=backend, browser=browser) as new_display: + yield new_display - # Module B - ComponentB = component_from_string( - """ - import React from "react"; - export function ComponentB({ children }) { - return React.createElement("div", { id: "component-b" }, children); - } - """, - "ComponentB", - name="module-b", - ) - @reactpy.component - def App(): - return ComponentA( - ComponentB( - reactpy.html.div({"id": "server-side"}, "Server Side Content") - ) - ) +async def test_nested_client_side_components(display: DisplayFixture): + # Module A + ComponentA = component_from_string( + """ + import React from "react"; + export function ComponentA({ children }) { + return React.createElement("div", { id: "component-a" }, children); + } + """, + "ComponentA", + name="module-a", + ) - await display.show(App) + # Module B + ComponentB = component_from_string( + """ + import React from "react"; + export function ComponentB({ children }) { + return React.createElement("div", { id: "component-b" }, children); + } + """, + "ComponentB", + name="module-b", + ) - # Check that all components are rendered - await display.page.wait_for_selector("#component-a") - await display.page.wait_for_selector("#component-b") - await display.page.wait_for_selector("#server-side") + @reactpy.component + def App(): + return ComponentA( + ComponentB(reactpy.html.div({"id": "server-side"}, "Server Side Content")) + ) + await display.show(App) -@pytest.mark.anyio -async def test_interleaved_client_server_components(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Module C - ComponentC = component_from_string( - """ - import React from "react"; - export function ComponentC({ children }) { - return React.createElement("div", { id: "component-c", className: "component-c" }, children); - } - """, - "ComponentC", - name="module-c", - ) + # Check that all components are rendered + await display.page.wait_for_selector("#component-a") + await display.page.wait_for_selector("#component-b") + await display.page.wait_for_selector("#server-side") + + +async def test_interleaved_client_server_components(display: DisplayFixture): + # Module C + ComponentC = component_from_string( + """ + import React from "react"; + export function ComponentC({ children }) { + return React.createElement("div", { id: "component-c", className: "component-c" }, children); + } + """, + "ComponentC", + name="module-c", + ) - @reactpy.component - def App(): - return reactpy.html.div( - {"id": "root-server"}, + @reactpy.component + def App(): + return reactpy.html.div( + {"id": "root-server"}, + ComponentC( + reactpy.html.div( + {"id": "nested-server"}, ComponentC( - reactpy.html.div( - {"id": "nested-server"}, - ComponentC( - reactpy.html.span({"id": "deep-server"}, "Deep Content") - ), - ) + reactpy.html.span({"id": "deep-server"}, "Deep Content") ), ) + ), + ) - await display.show(App) + await display.show(App) - await display.page.wait_for_selector("#root-server") - await display.page.wait_for_selector(".component-c") - await display.page.wait_for_selector("#nested-server") - # We need to check that there are two component-c elements - elements = await display.page.query_selector_all(".component-c") - assert len(elements) == 2 - await display.page.wait_for_selector("#deep-server") + await display.page.wait_for_selector("#root-server") + await display.page.wait_for_selector(".component-c") + await display.page.wait_for_selector("#nested-server") + # We need to check that there are two component-c elements + elements = await display.page.query_selector_all(".component-c") + assert len(elements) == 2 + await display.page.wait_for_selector("#deep-server") diff --git a/tests/test_reactjs/test_modules_from_npm.py b/tests/test_reactjs/test_modules_from_npm.py index 61c51f515..0b5fa468e 100644 --- a/tests/test_reactjs/test_modules_from_npm.py +++ b/tests/test_reactjs/test_modules_from_npm.py @@ -6,387 +6,345 @@ from reactpy.testing import BackendFixture, DisplayFixture -@pytest.mark.anyio -@pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_react_bootstrap(): +@pytest.fixture(scope="module") +async def display(browser): + """Override for the display fixture that includes ReactJS imports.""" async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Button = component_from_npm("react-bootstrap", "Button", version="2.10.2") + async with DisplayFixture(backend=backend, browser=browser) as new_display: + yield new_display + + +@pytest.mark.flaky(reruns=5, reruns_delay=2) +async def test_component_from_npm_react_bootstrap(display: DisplayFixture): + Button = component_from_npm("react-bootstrap", "Button", version="2.10.2") - @reactpy.component - def App(): - return Button({"variant": "primary", "id": "test-button"}, "Click me") + @reactpy.component + def App(): + return Button({"variant": "primary", "id": "test-button"}, "Click me") - await display.show(App) + await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" - # Check if it has the correct class for primary variant - # React Bootstrap buttons usually have 'btn' and 'btn-primary' classes - classes = await button.get_attribute("class") - assert "btn" in classes - assert "btn-primary" in classes + # Check if it has the correct class for primary variant + # React Bootstrap buttons usually have 'btn' and 'btn-primary' classes + classes = await button.get_attribute("class") + assert "btn" in classes + assert "btn-primary" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_material_ui(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Button = component_from_npm("@mui/material", "Button") +async def test_component_from_npm_material_ui(display: DisplayFixture): + Button = component_from_npm("@mui/material", "Button") - @reactpy.component - def App(): - return Button({"variant": "contained", "id": "test-button"}, "Click me") + @reactpy.component + def App(): + return Button({"variant": "contained", "id": "test-button"}, "Click me") - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - # Material UI transforms text to uppercase by default - assert await button.inner_text() == "CLICK ME" - classes = await button.get_attribute("class") - assert "MuiButton-root" in classes + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + # Material UI transforms text to uppercase by default + assert await button.inner_text() == "CLICK ME" + classes = await button.get_attribute("class") + assert "MuiButton-root" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_antd(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Try antd v4 which might be more stable with esm.sh - Button = component_from_npm("antd", "Button", version="4.24.15") +async def test_component_from_npm_antd(display: DisplayFixture): + # Try antd v4 which might be more stable with esm.sh + Button = component_from_npm("antd", "Button", version="4.24.15") - @reactpy.component - def App(): - return Button({"type": "primary", "id": "test-button"}, "Click me") + @reactpy.component + def App(): + return Button({"type": "primary", "id": "test-button"}, "Click me") - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert "Click me" in await button.inner_text() - classes = await button.get_attribute("class") - assert "ant-btn" in classes + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert "Click me" in await button.inner_text() + classes = await button.get_attribute("class") + assert "ant-btn" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_chakra_ui(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - ChakraProvider, Button = component_from_npm( - "@chakra-ui/react", ["ChakraProvider", "Button"], version="2.8.2" - ) +async def test_component_from_npm_chakra_ui(display: DisplayFixture): + ChakraProvider, Button = component_from_npm( + "@chakra-ui/react", ["ChakraProvider", "Button"], version="2.8.2" + ) - @reactpy.component - def App(): - return ChakraProvider( - Button({"colorScheme": "blue", "id": "test-button"}, "Click me") - ) + @reactpy.component + def App(): + return ChakraProvider( + Button({"colorScheme": "blue", "id": "test-button"}, "Click me") + ) - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" - classes = await button.get_attribute("class") - assert "chakra-button" in classes + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" + classes = await button.get_attribute("class") + assert "chakra-button" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_semantic_ui_react(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Button = component_from_npm("semantic-ui-react", "Button") +async def test_component_from_npm_semantic_ui_react(display: DisplayFixture): + Button = component_from_npm("semantic-ui-react", "Button") - @reactpy.component - def App(): - return Button({"primary": True, "id": "test-button"}, "Click me") + @reactpy.component + def App(): + return Button({"primary": True, "id": "test-button"}, "Click me") - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" - classes = await button.get_attribute("class") - assert "ui" in classes - assert "button" in classes + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" + classes = await button.get_attribute("class") + assert "ui" in classes + assert "button" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_mantine(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - MantineProvider, Button = component_from_npm( - "@mantine/core", ["MantineProvider", "Button"], version="7.3.0" - ) +async def test_component_from_npm_mantine(display: DisplayFixture): + MantineProvider, Button = component_from_npm( + "@mantine/core", ["MantineProvider", "Button"], version="7.3.0" + ) - @reactpy.component - def App(): - return MantineProvider(Button({"id": "test-button"}, "Click me")) + @reactpy.component + def App(): + return MantineProvider(Button({"id": "test-button"}, "Click me")) - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" - classes = await button.get_attribute("class") - assert "mantine-Button-root" in classes + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" + classes = await button.get_attribute("class") + assert "mantine-Button-root" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_fluent_ui(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - PrimaryButton = component_from_npm("@fluentui/react", "PrimaryButton") +async def test_component_from_npm_fluent_ui(display: DisplayFixture): + PrimaryButton = component_from_npm("@fluentui/react", "PrimaryButton") - @reactpy.component - def App(): - return PrimaryButton({"id": "test-button"}, "Click me") + @reactpy.component + def App(): + return PrimaryButton({"id": "test-button"}, "Click me") - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" - classes = await button.get_attribute("class") - assert "ms-Button" in classes + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" + classes = await button.get_attribute("class") + assert "ms-Button" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_blueprint(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Button = component_from_npm("@blueprintjs/core", "Button") +async def test_component_from_npm_blueprint(display: DisplayFixture): + Button = component_from_npm("@blueprintjs/core", "Button") - @reactpy.component - def App(): - return Button({"intent": "primary", "id": "test-button"}, "Click me") + @reactpy.component + def App(): + return Button({"intent": "primary", "id": "test-button"}, "Click me") - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" - classes = await button.get_attribute("class") - assert any(c.startswith("bp") and "button" in c for c in classes.split()) + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" + classes = await button.get_attribute("class") + assert any(c.startswith("bp") and "button" in c for c in classes.split()) -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_grommet(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Grommet, Button = component_from_npm("grommet", ["Grommet", "Button"]) +async def test_component_from_npm_grommet(display: DisplayFixture): + Grommet, Button = component_from_npm("grommet", ["Grommet", "Button"]) - @reactpy.component - def App(): - return Grommet( - Button({"primary": True, "label": "Click me", "id": "test-button"}) - ) + @reactpy.component + def App(): + return Grommet( + Button({"primary": True, "label": "Click me", "id": "test-button"}) + ) - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_evergreen(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Button = component_from_npm("evergreen-ui", "Button") +async def test_component_from_npm_evergreen(display: DisplayFixture): + Button = component_from_npm("evergreen-ui", "Button") - @reactpy.component - def App(): - return Button( - {"appearance": "primary", "id": "test-button"}, "Click me" - ) + @reactpy.component + def App(): + return Button({"appearance": "primary", "id": "test-button"}, "Click me") - await display.show(App) - button = await display.page.wait_for_selector("#test-button") - assert await button.inner_text() == "Click me" + await display.show(App) + button = await display.page.wait_for_selector("#test-button") + assert await button.inner_text() == "Click me" -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_component_from_npm_react_spinners(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - ClipLoader = component_from_npm("react-spinners", "ClipLoader") - - @reactpy.component - def App(): - return ClipLoader( - { - "color": "red", - "loading": True, - "size": 150, - "data-testid": "loader", - } - ) - - await display.show(App) - # react-spinners renders a span with the loader - # We can check if it exists. It might not have an ID we can easily set on the root if it doesn't forward props well, - # but let's try wrapping it. - loader = await display.page.wait_for_selector("span[data-testid='loader']") - assert await loader.is_visible() - - -@pytest.mark.anyio +async def test_component_from_npm_react_spinners(display: DisplayFixture): + ClipLoader = component_from_npm("react-spinners", "ClipLoader") + + @reactpy.component + def App(): + return ClipLoader( + { + "color": "red", + "loading": True, + "size": 150, + "data-testid": "loader", + } + ) + + await display.show(App) + # react-spinners renders a span with the loader + # We can check if it exists. It might not have an ID we can easily set on the root if it doesn't forward props well, + # but let's try wrapping it. + loader = await display.page.wait_for_selector("span[data-testid='loader']") + assert await loader.is_visible() + + @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_nested_npm_components(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Use Chakra UI Provider and Box, and nest a React Bootstrap Button inside - ChakraProvider, Box = component_from_npm( - "@chakra-ui/react", ["ChakraProvider", "Box"], version="2.8.2" - ) - BootstrapButton = component_from_npm( - "react-bootstrap", "Button", version="2.10.2" +async def test_nested_npm_components(display: DisplayFixture): + # Use Chakra UI Provider and Box, and nest a React Bootstrap Button inside + ChakraProvider, Box = component_from_npm( + "@chakra-ui/react", ["ChakraProvider", "Box"], version="2.8.2" + ) + BootstrapButton = component_from_npm("react-bootstrap", "Button", version="2.10.2") + + @reactpy.component + def App(): + return ChakraProvider( + Box( + { + "id": "chakra-box", + "p": 4, + "color": "white", + "bg": "blue.500", + }, + BootstrapButton( + {"variant": "light", "id": "bootstrap-button"}, + "Nested Button", + ), ) + ) - @reactpy.component - def App(): - return ChakraProvider( - Box( - { - "id": "chakra-box", - "p": 4, - "color": "white", - "bg": "blue.500", - }, - BootstrapButton( - {"variant": "light", "id": "bootstrap-button"}, - "Nested Button", - ), - ) - ) - - await display.show(App) + await display.show(App) - box = await display.page.wait_for_selector("#chakra-box") - assert await box.is_visible() + box = await display.page.wait_for_selector("#chakra-box") + assert await box.is_visible() - button = await display.page.wait_for_selector("#bootstrap-button") - assert await button.inner_text() == "Nested Button" - classes = await button.get_attribute("class") - assert "btn" in classes + button = await display.page.wait_for_selector("#bootstrap-button") + assert await button.inner_text() == "Nested Button" + classes = await button.get_attribute("class") + assert "btn" in classes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_interleaved_npm_and_server_components(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - Card = component_from_npm("antd", "Card", version="4.24.15") - Button = component_from_npm("@mui/material", "Button") - - @reactpy.component - def App(): - return Card( - {"title": "Antd Card", "id": "antd-card"}, - html.div( - { - "id": "server-div", - "style": {"padding": "10px", "border": "1px solid red"}, - }, - "Server Side Div", - Button( - {"variant": "contained", "id": "mui-button"}, "MUI Button" - ), - ), - ) +async def test_interleaved_npm_and_server_components(display: DisplayFixture): + Card = component_from_npm("antd", "Card", version="4.24.15") + Button = component_from_npm("@mui/material", "Button") + + @reactpy.component + def App(): + return Card( + {"title": "Antd Card", "id": "antd-card"}, + html.div( + { + "id": "server-div", + "style": {"padding": "10px", "border": "1px solid red"}, + }, + "Server Side Div", + Button({"variant": "contained", "id": "mui-button"}, "MUI Button"), + ), + ) - await display.show(App) + await display.show(App) - card = await display.page.wait_for_selector("#antd-card") - assert await card.is_visible() + card = await display.page.wait_for_selector("#antd-card") + assert await card.is_visible() - server_div = await display.page.wait_for_selector("#server-div") - assert await server_div.is_visible() - assert "Server Side Div" in await server_div.inner_text() + server_div = await display.page.wait_for_selector("#server-div") + assert await server_div.is_visible() + assert "Server Side Div" in await server_div.inner_text() - button = await display.page.wait_for_selector("#mui-button") - assert "MUI BUTTON" in await button.inner_text() # MUI capitalizes + button = await display.page.wait_for_selector("#mui-button") + assert "MUI BUTTON" in await button.inner_text() # MUI capitalizes -@pytest.mark.anyio @pytest.mark.flaky(reruns=5, reruns_delay=2) -async def test_complex_nested_material_ui(): - async with BackendFixture(html_head=html.head(import_reactjs())) as backend: - async with DisplayFixture(backend=backend) as display: - # Import multiple components from @mui/material - # Note: component_from_npm can take a list of names - mui_components = component_from_npm( - "@mui/material", - ["Button", "Card", "CardContent", "Typography", "Box", "Stack"], - ) - Button, Card, CardContent, Typography, Box, Stack = mui_components - - @reactpy.component - def App(): - return Box( - { - "sx": { - "padding": "20px", - "backgroundColor": "#f5f5f5", - "height": "100vh", - } - }, - Stack( - {"spacing": 2, "direction": "column", "alignItems": "center"}, +async def test_complex_nested_material_ui(display: DisplayFixture): + # Import multiple components from @mui/material + # Note: component_from_npm can take a list of names + mui_components = component_from_npm( + "@mui/material", + ["Button", "Card", "CardContent", "Typography", "Box", "Stack"], + ) + Button, Card, CardContent, Typography, Box, Stack = mui_components + + @reactpy.component + def App(): + return Box( + { + "sx": { + "padding": "20px", + "backgroundColor": "#f5f5f5", + "height": "100vh", + } + }, + Stack( + {"spacing": 2, "direction": "column", "alignItems": "center"}, + Typography( + {"variant": "h4", "component": "h1", "gutterBottom": True}, + "Complex Nested UI Test", + ), + Card( + {"sx": {"minWidth": 300, "maxWidth": 500}}, + CardContent( Typography( - {"variant": "h4", "component": "h1", "gutterBottom": True}, - "Complex Nested UI Test", + { + "sx": {"fontSize": 14}, + "color": "text.secondary", + "gutterBottom": True, + }, + "Word of the Day", ), - Card( - {"sx": {"minWidth": 300, "maxWidth": 500}}, - CardContent( - Typography( - { - "sx": {"fontSize": 14}, - "color": "text.secondary", - "gutterBottom": True, - }, - "Word of the Day", - ), - Typography( - {"variant": "h5", "component": "div"}, - "be-nev-o-lent", - ), - Typography( - {"sx": {"mb": 1.5}, "color": "text.secondary"}, - "adjective", - ), - Typography( - {"variant": "body2"}, "well meaning and kindly." - ), - ), - Box( - { - "sx": { - "padding": "10px", - "display": "flex", - "justifyContent": "flex-end", - } - }, - Button( - { - "size": "small", - "variant": "contained", - "id": "learn-more-btn", - }, - "Learn More", - ), - ), + Typography( + {"variant": "h5", "component": "div"}, + "be-nev-o-lent", ), + Typography( + {"sx": {"mb": 1.5}, "color": "text.secondary"}, + "adjective", + ), + Typography({"variant": "body2"}, "well meaning and kindly."), ), - ) - - await display.show(App) - - # Check if the button is visible and has correct text - btn = await display.page.wait_for_selector("#learn-more-btn") - assert await btn.is_visible() - # Material UI transforms text to uppercase by default - assert "LEARN MORE" in await btn.inner_text() - - # Check if Card is rendered (it usually has MuiCard-root class) - # We can't easily select by ID as we didn't put one on Card, but we can check structure if needed. - # But let's just check if the text "be-nev-o-lent" is visible - text = await display.page.wait_for_selector("text=be-nev-o-lent") - assert await text.is_visible() + Box( + { + "sx": { + "padding": "10px", + "display": "flex", + "justifyContent": "flex-end", + } + }, + Button( + { + "size": "small", + "variant": "contained", + "id": "learn-more-btn", + }, + "Learn More", + ), + ), + ), + ), + ) + + await display.show(App) + + # Check if the button is visible and has correct text + btn = await display.page.wait_for_selector("#learn-more-btn") + assert await btn.is_visible() + # Material UI transforms text to uppercase by default + assert "LEARN MORE" in await btn.inner_text() + + # Check if Card is rendered (it usually has MuiCard-root class) + # We can't easily select by ID as we didn't put one on Card, but we can check structure if needed. + # But let's just check if the text "be-nev-o-lent" is visible + text = await display.page.wait_for_selector("text=be-nev-o-lent") + assert await text.is_visible() diff --git a/tests/test_reactjs/test_utils.py b/tests/test_reactjs/test_utils.py index c22ad80a0..e5a00f550 100644 --- a/tests/test_reactjs/test_utils.py +++ b/tests/test_reactjs/test_utils.py @@ -1,9 +1,12 @@ from pathlib import Path +from unittest.mock import patch import pytest import responses from reactpy.reactjs.utils import ( + copy_file, + file_lock, module_name_suffix, normalize_url_path, resolve_from_module_file, @@ -163,3 +166,35 @@ def test_resolve_relative_url(): == "https://some.url/path/to/another.js" ) assert normalize_url_path("/some/path", "to/another.js") == "to/another.js" + + +def test_copy_file_fallback(tmp_path): + source = tmp_path / "source.txt" + source.write_text("content") + target = tmp_path / "target.txt" + + path_cls = type(target) + + with patch("shutil.copy"): + with patch.object( + path_cls, "replace", side_effect=[OSError, OSError] + ) as mock_replace: + with patch.object(path_cls, "rename") as mock_rename: + with patch.object(path_cls, "exists", return_value=True): + with patch.object(path_cls, "unlink") as mock_unlink: + with patch("time.sleep"): # Speed up test + copy_file(target, source, symlink=False) + + assert mock_replace.call_count == 2 + mock_unlink.assert_called_once() + mock_rename.assert_called_once() + + +def test_simple_file_lock_timeout(tmp_path): + lock_file = tmp_path / "lock" + + with patch("os.open", side_effect=OSError): + with patch("time.sleep"): # Speed up test + with pytest.raises(TimeoutError, match="Could not acquire lock"): + with file_lock(lock_file, timeout=0.1): + pass diff --git a/tests/test_testing.py b/tests/test_testing.py index 3318bb2c4..60435a8cf 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,11 +1,12 @@ import logging import os +from unittest.mock import AsyncMock, MagicMock, patch import pytest from reactpy import Ref, component, html, testing from reactpy.logging import ROOT_LOGGER -from reactpy.testing.backend import _hotswap +from reactpy.testing.backend import BackendFixture, _hotswap from reactpy.testing.display import DisplayFixture from tests.sample import SampleApp @@ -135,15 +136,13 @@ def test_assert_reactpy_did_not_log(): ROOT_LOGGER.exception("something") -async def test_simple_display_fixture(): - if os.name == "nt": - pytest.skip("Browser tests not supported on Windows") - async with testing.DisplayFixture() as display: +async def test_simple_display_fixture(browser): + async with testing.DisplayFixture(browser=browser) as display: await display.show(SampleApp) await display.page.wait_for_selector("#sample") -def test_list_logged_excptions(): +def test_list_logged_exceptions(): the_error = None with testing.capture_reactpy_logs() as records: ROOT_LOGGER.info("A non-error log message") @@ -207,3 +206,91 @@ async def on_click(event): await display.page.wait_for_selector("#hotswap-2") await client_incr_button.click() await display.page.wait_for_selector("#hotswap-3") + + +@pytest.mark.asyncio +async def test_backend_server_failure(): + # We need to mock uvicorn.Server to fail starting + with patch("uvicorn.Server") as mock_server_cls: + mock_server = mock_server_cls.return_value + mock_server.started = False + mock_server.servers = [] + mock_server.config.get_loop_factory = MagicMock() + + # Mock serve to just return (or sleep briefly then return) + mock_server.serve = AsyncMock(return_value=None) + + backend = BackendFixture() + + # We need to speed up the loop + with patch("asyncio.sleep", new_callable=AsyncMock): + with pytest.raises(RuntimeError, match="Server failed to start"): + await backend.__aenter__() + + +@pytest.mark.asyncio +async def test_display_fixture_headless_logic(): + # Mock async_playwright to avoid launching real browser + with patch("reactpy.testing.display.async_playwright") as mock_pw: + mock_context_manager = mock_pw.return_value + mock_playwright_instance = AsyncMock() + mock_context_manager.__aenter__.return_value = mock_playwright_instance + + mock_browser = AsyncMock() + mock_browser.__aenter__ = AsyncMock(return_value=mock_browser) + mock_playwright_instance.chromium.launch.return_value = mock_browser + + mock_page = AsyncMock() + # Configure synchronous methods on page + mock_page.set_default_timeout = MagicMock() + mock_page.set_default_navigation_timeout = MagicMock() + mock_page.on = MagicMock() + mock_page.__aenter__ = AsyncMock(return_value=mock_page) + + mock_browser.new_page.return_value = mock_page + + # Case: headless=False, PLAYWRIGHT_VISIBLE='1' + with patch.dict(os.environ, {"PLAYWRIGHT_VISIBLE": "1"}): + async with DisplayFixture(): + pass + mock_playwright_instance.chromium.launch.assert_called_with(headless=False) + + # Case: headless=True, PLAYWRIGHT_VISIBLE='0' + with patch.dict(os.environ, {"PLAYWRIGHT_VISIBLE": "0"}): + async with DisplayFixture(): + pass + mock_playwright_instance.chromium.launch.assert_called_with(headless=True) + + +@pytest.mark.asyncio +async def test_display_fixture_internal_backend(): + # This covers line 87: await self.backend_exit_stack.aclose() + # when backend is internal (default) + + with patch("reactpy.testing.display.async_playwright") as mock_pw: + mock_context_manager = mock_pw.return_value + mock_playwright_instance = AsyncMock() + mock_context_manager.__aenter__.return_value = mock_playwright_instance + + mock_browser = AsyncMock() + mock_browser.__aenter__ = AsyncMock(return_value=mock_browser) + mock_playwright_instance.chromium.launch.return_value = mock_browser + + mock_page = AsyncMock() + mock_page.set_default_timeout = MagicMock() + mock_page.set_default_navigation_timeout = MagicMock() + mock_page.on = MagicMock() + mock_page.__aenter__ = AsyncMock(return_value=mock_page) + mock_browser.new_page.return_value = mock_page + + # We also need to mock BackendFixture to avoid starting real server + with patch("reactpy.testing.display.BackendFixture") as mock_backend_cls: + mock_backend = AsyncMock() + mock_backend.mount = MagicMock() # mount is synchronous + mock_backend_cls.return_value = mock_backend + + async with DisplayFixture() as display: + assert not display.backend_is_external + + # Verify backend exit stack closed (implied if no error and backend.__aexit__ called) + mock_backend.__aexit__.assert_called() diff --git a/tests/test_utils.py b/tests/test_utils.py index d98adab6b..52cea0f6a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -275,7 +275,12 @@ def test_non_html_tag_behavior(): utils.string_to_reactpy(source, strict=True) -SOME_OBJECT = object() +class StableReprObject: + def __repr__(self): + return "StableReprObject" + + +SOME_OBJECT = StableReprObject() @component diff --git a/tests/test_web/js_fixtures/subcomponent-notation.js b/tests/test_web/js_fixtures/subcomponent-notation.js index 12542786e..4bb2f89b6 100644 --- a/tests/test_web/js_fixtures/subcomponent-notation.js +++ b/tests/test_web/js_fixtures/subcomponent-notation.js @@ -1,17 +1,11 @@ -import React from "https://esm.sh/v135/react@19.0" -import ReactDOM from "https://esm.sh/v135/react-dom@19.0/client" -// Explicitly import react-is to ensure it's loaded before react-bootstrap -// This prevents race conditions where react-bootstrap tries to use React context before deps are ready -import * as ReactIs from "https://esm.sh/v135/react-is@19.0" -import {InputGroup, Form} from "https://esm.sh/v135/react-bootstrap@2.10.2?deps=react@19.0,react-dom@19.0,react-is@19.0&exports=InputGroup,Form"; -export {InputGroup, Form}; +import React from "react"; +import ReactDOM from "react-dom"; -export function bind(node, config) { - const root = ReactDOM.createRoot(node); - return { - create: (type, props, children) => - React.createElement(type, props, ...children), - render: (element) => root.render(element), - unmount: () => root.unmount() - }; -} \ No newline at end of file +const InputGroup = ({ children }) => React.createElement("div", { className: "input-group" }, children); +InputGroup.Text = ({ children, ...props }) => React.createElement("span", { className: "input-group-text", ...props }, children); + +const Form = ({ children }) => React.createElement("form", {}, children); +Form.Control = ({ children, ...props }) => React.createElement("input", { className: "form-control", ...props }, children); +Form.Label = ({ children, ...props }) => React.createElement("label", { className: "form-label", ...props }, children); + +export { InputGroup, Form }; diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index ae751b69e..cddcc86dc 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -8,7 +8,7 @@ import reactpy import reactpy.reactjs from reactpy.executors.asgi.standalone import ReactPy -from reactpy.reactjs import NAME_SOURCE, JavaScriptModule +from reactpy.reactjs import NAME_SOURCE, JavaScriptModule, import_reactjs from reactpy.testing import ( BackendFixture, DisplayFixture, @@ -21,6 +21,14 @@ JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures" +@pytest.fixture(scope="module") +async def display(browser): + """Override for the display fixture that includes ReactJS.""" + async with BackendFixture(html_head=reactpy.html.head(import_reactjs())) as backend: + async with DisplayFixture(backend=backend, browser=browser) as new_display: + yield new_display + + async def test_that_js_module_unmount_is_called(display: DisplayFixture): SomeComponent = reactpy.reactjs.module_to_vdom( reactpy.reactjs.file_to_module( @@ -70,7 +78,7 @@ def ShowSimpleButton(): app = ServeStaticASGI(app, JS_FIXTURES_DIR, "/static/") async with BackendFixture(app) as server: - async with DisplayFixture(server, browser) as display: + async with DisplayFixture(server, browser=browser) as display: await display.show(ShowSimpleButton) await display.page.wait_for_selector("#my-button") @@ -103,30 +111,36 @@ def test_module_from_file_source_conflict(tmp_path): first_file = tmp_path / "first.js" with pytest.raises(FileNotFoundError, match=r"does not exist"): - reactpy.reactjs.file_to_module("temp", first_file) + reactpy.reactjs.file_to_module( + "test-module-from-file-source-conflict", first_file + ) first_file.touch() - reactpy.reactjs.file_to_module("temp", first_file) + reactpy.reactjs.file_to_module("test-module-from-file-source-conflict", first_file) second_file = tmp_path / "second.js" second_file.touch() # ok, same content - reactpy.reactjs.file_to_module("temp", second_file) + reactpy.reactjs.file_to_module("test-module-from-file-source-conflict", second_file) third_file = tmp_path / "third.js" third_file.write_text("something-different") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.reactjs.file_to_module("temp", third_file) + reactpy.reactjs.file_to_module( + "test-module-from-file-source-conflict", third_file + ) def test_web_module_from_file_symlink(tmp_path): file = tmp_path / "temp.js" file.touch() - module = reactpy.reactjs.file_to_module("temp", file, symlink=True) + module = reactpy.reactjs.file_to_module( + "test-web-module-from-file-symlink", file, symlink=True + ) assert module.file.resolve().read_text() == "" @@ -139,29 +153,38 @@ def test_web_module_from_file_symlink_twice(tmp_path): file_1 = tmp_path / "temp_1.js" file_1.touch() - reactpy.reactjs.file_to_module("temp", file_1, symlink=True) - - with assert_reactpy_did_not_log(r"Existing web module .* will be replaced with"): - reactpy.reactjs.file_to_module("temp", file_1, symlink=True) - + reactpy.reactjs.file_to_module( + "test-web-module-from-file-symlink-twice", file_1, symlink=True + ) + with assert_reactpy_did_not_log( + r"Existing web module 'test-web-module-from-file-symlink-twice.js' will be replaced with" + ): + reactpy.reactjs.file_to_module( + "test-web-module-from-file-symlink-twice", file_1, symlink=True + ) file_2 = tmp_path / "temp_2.js" file_2.write_text("something") - - with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.reactjs.file_to_module("temp", file_2, symlink=True) + with assert_reactpy_did_log( + r"Existing web module 'test-web-module-from-file-symlink-twice.js' will be replaced with" + ): + reactpy.reactjs.file_to_module( + "test-web-module-from-file-symlink-twice", file_2, symlink=True + ) def test_web_module_from_file_replace_existing(tmp_path): file1 = tmp_path / "temp1.js" file1.touch() - reactpy.reactjs.file_to_module("temp", file1) + reactpy.reactjs.file_to_module("test-web-module-from-file-replace-existing", file1) file2 = tmp_path / "temp2.js" file2.write_text("something") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): - reactpy.reactjs.file_to_module("temp", file2) + reactpy.reactjs.file_to_module( + "test-web-module-from-file-replace-existing", file2 + ) def test_module_missing_exports(): @@ -325,7 +348,6 @@ async def test_subcomponent_notation_as_str_attrs(display: DisplayFixture): ) await display.show(lambda: content) - await display.page.wait_for_selector("#basic-addon3", state="attached") parent = await display.page.wait_for_selector("#the-parent", state="attached") input_group_text = await parent.query_selector_all(".input-group-text") @@ -418,12 +440,12 @@ def App(): def test_component_from_string(): reactpy.reactjs.component_from_string( - "old", "Component", resolve_imports=False, name="temp" + "old", "Component", resolve_imports=False, name="test-component-from-string" ) reactpy.reactjs._STRING_JS_MODULE_CACHE.clear() with assert_reactpy_did_log(r"Existing web module .* will be replaced with"): reactpy.reactjs.component_from_string( - "new", "Component", resolve_imports=False, name="temp" + "new", "Component", resolve_imports=False, name="test-component-from-string" ) @@ -440,7 +462,7 @@ def ShowSimpleButton(): app = ServeStaticASGI(app, JS_FIXTURES_DIR, "/static/") async with BackendFixture(app) as server: - async with DisplayFixture(server, browser) as display: + async with DisplayFixture(server, browser=browser) as display: await display.show(ShowSimpleButton) await display.page.wait_for_selector("#my-button")