Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 14 additions & 39 deletions .github/workflows/integration_app_harness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ on:
paths-ignore:
- "**/*.md"
env:
APP_HARNESS_HEADLESS: 1
PYTHONUNBUFFERED: 1

permissions:
Expand Down Expand Up @@ -53,46 +52,22 @@ jobs:
python-version: ${{ matrix.python-version }}
run-uv-sync: true

- name: Run app harness tests
env:
REFLEX_REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: uv run pytest tests/integration --ignore=tests/integration/tests_playwright --reruns 3 -v --maxfail=5 --splits 2 --group ${{matrix.split_index}}

# Playwright tests run in a separate job because the pytest-playwright plugin
# keeps an asyncio event loop running on the main thread for the entire
# session, which is incompatible with pytest-asyncio tests.
integration-app-harness-playwright:
timeout-minutes: 30
strategy:
matrix:
state_manager: ["redis", "memory"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
fail-fast: false
runs-on: ubuntu-22.04
services:
redis:
image: ${{ matrix.state_manager == 'redis' && 'redis' || '' }}
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
- uses: ./.github/actions/setup_build_env
with:
python-version: ${{ matrix.python-version }}
run-uv-sync: true

- name: Install playwright
run: uv run playwright install chromium --only-shell

- name: Run playwright tests
- name: Diagnostics
continue-on-error: true
run: |
uv pip list | grep -iE 'pytest|playwright|asyncio' || true
uv run pytest --version
uv run playwright --version
ls -la ~/.cache/ms-playwright/ || true
uv run python -c "import pytest_playwright; print('pytest_playwright loaded')"

- name: Run app harness tests
env:
REFLEX_REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: uv run pytest tests/integration/tests_playwright --reruns 3 -v --maxfail=5
# Disable pytest-asyncio: sync Playwright's session event loop can't
# coexist with pytest-asyncio's runner. None of the integration
# tests use `async def`, so the plugin is not needed.
run: uv run pytest tests/integration -p no:asyncio --reruns 3 -v --maxfail=5 --splits 2 --group ${{matrix.split_index}}
1 change: 0 additions & 1 deletion .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ env:
REFLEX_TELEMETRY_ENABLED: false
NODE_OPTIONS: "--max_old_space_size=8192"
PR_TITLE: ${{ github.event.pull_request.title }}
APP_HARNESS_HEADLESS: 1
PYTHONUNBUFFERED: 1

jobs:
Expand Down
27 changes: 21 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ uv run pre-commit run --all-files # all pre-commi
reflex/ # main framework package (app, state, compiler, components, utils, istate)
packages/ # workspace sub-packages (reflex-base, reflex-components-*, reflex-docgen, reflex-components-internal)
tests/units/ # unit tests, mirrors source tree
tests/integration/ # Selenium integration tests (run in dev+prod modes)
tests_playwright/ # Playwright integration tests (preferred for new tests)
tests/integration/ # Playwright integration tests (run in dev+prod modes via app_harness_env)
tests/benchmarks/ # performance benchmarks
docs/ # documentation site (separate workspace member)
```
Expand All @@ -53,21 +52,29 @@ docs/ # documentation site (separate workspace member)
- Test functions at module level, not wrapped in classes.
- **Unit tests:** `tests/units/`, run with `uv run pytest tests/units`.
- unit tests should primarily cover a single module, and should be named accordingly, including subdirectories (e.g. `tests/units/istate/test_manager.py` for `reflex/istate/manager.py`). For subpackages, also include the corresponding path below `src/` (e.g. `tests/units/reflex_base/event/test_context.py` for `packages/reflex-base/src/reflex_base/event/context.py`).
- **Integration tests:** prefer Playwright (`tests/integration/tests_playwright/`). Integration tests are slow — extend existing test apps rather than creating new ones for trivial functionality. Multiple test cases sharing one app is fine.
- **Integration tests:** `tests/integration/`, all written with **sync Playwright** (`from playwright.sync_api import Page, expect`). Selenium is no longer supported for `AppHarness`-based tests. Integration tests are slow — extend existing test apps rather than creating new ones for trivial functionality. Multiple test cases sharing one app is fine.

### Integration test patterns

Apps as factory functions, run via `AppHarness`:
Apps as factory functions, run via `AppHarness`. Tests use the pytest-playwright `page` fixture and navigate to `harness.frontend_url`. Utilities in `tests/integration/utils.py` (token polling, navigation, event ordering, LocalStorage/SessionStorage).

```python
from collections.abc import Generator

import pytest
from playwright.sync_api import Page, expect

from reflex.testing import AppHarness


def SomeApp():
import reflex as rx

class State(rx.State):
value: str = ""

def index():
return rx.box(rx.text(State.value))
return rx.box(rx.text(State.value, id="value"))

app = rx.App()
app.add_page(index)
Expand All @@ -79,9 +86,17 @@ def some_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
root=tmp_path_factory.mktemp("some_app"), app_source=SomeApp
) as harness:
yield harness


def test_value(some_app: AppHarness, page: Page):
assert some_app.frontend_url is not None
page.goto(some_app.frontend_url)
expect(page.locator("#value")).to_have_text("")
```

Playwright tests use the `page` fixture and navigate to `harness.frontend_url`. Utilities in `tests/integration/utils.py` (polling, event ordering, storage).
**Fixture scope:** every `AppHarness`-returning fixture must be `scope="module"` so the app server actually shuts down after the module's tests finish. The `page` fixture is function-scoped (new Playwright page per test).

**Dev+prod parametrization:** for tests that should exercise both development and production builds, take the shared `app_harness_env` fixture from `tests/integration/conftest.py` and call `app_harness_env.create(...)` instead of `AppHarness.create(...)`.

## .pyi stubs

Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ Within the 'test' directory of Reflex you can add to a test file already there o
- Any edge cases or potential problem areas.
- Any interactions between different parts of the code.

#### Integration tests

Integration tests live in `tests/integration/` and exercise a real Reflex app in a browser. They are written with **sync Playwright** on top of `reflex.testing.AppHarness`; Selenium is no longer supported. See `AGENTS.md` for the idiomatic patterns, and reuse the helpers in `tests/integration/utils.py` (`poll_for_token`, `poll_for_navigation`, `LocalStorage`, `SessionStorage`, `poll_assert_event_order`, etc.) rather than inlining ad-hoc polling.

Each `AppHarness`-returning fixture should be `scope="module"` so the app server shuts down only after every test in the module has finished.

## ✅ Making a PR

Once you solve a current issue or improvement to Reflex, you can make a PR, and we will review the changes.
Expand Down
9 changes: 0 additions & 9 deletions packages/reflex-base/src/reflex_base/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,15 +713,6 @@ class EnvironmentVariables:
# If this env var is set to "yes", App.compile will be a no-op
REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)

# Whether to run app harness tests in headless mode.
APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False)

# Which app harness driver to use.
APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome")

# Arguments to pass to the app harness driver.
APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("")

# Whether to check for outdated package versions.
REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)

Expand Down
36 changes: 36 additions & 0 deletions pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,42 @@
"packages/reflex-components-core/src/reflex_components_core/react_router/dom.pyi": "1074a512195ae23d479c4a2d553954e1",
"packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "8e379fa038c7c6c0672639eb5902934d",
"packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "d2dc211d707c402eb24678a4cba945f7",
"packages/reflex-components-internal/src/reflex_components_internal/__init__.pyi": "0a9b377ab3b73af0a1f426b1552a67b1",
"packages/reflex-components-internal/src/reflex_components_internal/blocks/calcom.pyi": "ee9f90b28fca6389551ae1f6a79166de",
"packages/reflex-components-internal/src/reflex_components_internal/blocks/plain.pyi": "6b9eb619dd1525ceb420666a592b6c7c",
"packages/reflex-components-internal/src/reflex_components_internal/components/__init__.pyi": "12a56da68a8cd02d030f2bc4371b912f",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/__init__.pyi": "90c52e82b3e83652d46243fd78007f90",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/accordion.pyi": "6af36d177f8227724bbf076056b630a8",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/avatar.pyi": "997885494e0d79fb8288a8a0c00ebebb",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/badge.pyi": "93acd8d72286962e554c5412c40f1f61",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/button.pyi": "bc49660d2a483282629dd602e74b19a9",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/card.pyi": "c94befb300814d5516524fc04ecfd847",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/checkbox.pyi": "85c4822ce07e9ffb85efc1ff833cc287",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/collapsible.pyi": "8c146c6f0cd476f0f44bf5ffe9e78ad1",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/context_menu.pyi": "6728b7e125a86ed8d052b77e2da49dad",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/dialog.pyi": "e0640a8f267ee651f80873540a0a585d",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/drawer.pyi": "dd8fc5c5f9f2cb98d969cbd6d04c8ddb",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/gradient_profile.pyi": "7cd4cb0967ffc0168ca53fad87417e05",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/input.pyi": "871a7a6adf54b7d861972d27c85df0ee",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/link.pyi": "e6a233240615b269a0787c92c7ab5a49",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/menu.pyi": "12d859e49e18f2667be9414589cf41e7",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/navigation_menu.pyi": "ba36f42b7f0ea516f60f93f2cea628cb",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/popover.pyi": "994411bd94873fc5fde4830d35003a3e",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/preview_card.pyi": "2e966f2d8c708bfdc92207730c51ccb6",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/scroll_area.pyi": "8619d779ea26b33cf9ee0c2ff9c40abf",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/select.pyi": "658c4955a959b704ca817eb308384000",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/slider.pyi": "7fd79434ad40c52111a23c6b1800c591",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/switch.pyi": "d4d5ac7cf96dc4c304aea95b86c18c3f",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/tabs.pyi": "ed10dfc15c9ee5866a92e1bc4497c91e",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/textarea.pyi": "891dba7cf500a4cc1ca6c704fed6056c",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/toggle.pyi": "2dabedfa2aee3f05f2a0a50ab7e44864",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/toggle_group.pyi": "11d9db2ad00042ed9c3a99a41f5ec3b9",
"packages/reflex-components-internal/src/reflex_components_internal/components/base/tooltip.pyi": "0ecb8bcafa7a5958cd8a5649e3eafb0e",
"packages/reflex-components-internal/src/reflex_components_internal/components/base_ui.pyi": "41d3ae3d1f082f4a17183af182703a52",
"packages/reflex-components-internal/src/reflex_components_internal/components/icons/__init__.pyi": "acc2a513209c74373cedc17a9fdb232b",
"packages/reflex-components-internal/src/reflex_components_internal/components/icons/hugeicon.pyi": "b82885f35d0fa394330c891234ef0a40",
"packages/reflex-components-internal/src/reflex_components_internal/components/icons/simple_icon.pyi": "7a0774976e45f967d1ee9840b6163f19",
"packages/reflex-components-internal/src/reflex_components_internal/utils/__init__.pyi": "9d029297b54797696c72c43342130222",
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "e3ec310276f9d091fbb0261e523ca9ed",
"packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "da02f81678d920a68101c08fe64483a5",
"packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "d6a02e447dfd3c91bba84bcd02722aed",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ reflex = "reflex.reflex:cli"
dev = [
"alembic",
"asynctest",
"attrs",
"codespell",
"darglint",
"dill",
Expand Down Expand Up @@ -106,7 +107,6 @@ dev = [
"python-dotenv",
"reflex-docgen",
"ruff",
"selenium",
"sqlalchemy",
"sqlmodel",
"starlette-admin",
Expand Down
Loading
Loading