Skip to content

Commit 2f7ca21

Browse files
Add pytest-textual-snapshot and integration tests (#259)
* Add pytest-textual-snapshot and integration tests - Add pytest-textual-snapshot for visual regression testing of Textual UI - Create snapshot tests for ExitModal, InputField, OpenHandsApp, ConfirmationModal - Add integration test fixtures for real LLM testing via env vars - Create integration tests that send messages to real LLM - Register 'integration' pytest marker in pyproject.toml - Document snapshot and integration testing in repo.md Co-authored-by: openhands <openhands@all-hands.dev> * Fix CI failures: format code and mock WORK_DIR in snapshot test - Apply ruff formatting to tests/conftest.py and tests/integration/test_cli_with_real_llm.py - Mock WORK_DIR in test_openhands_app_splash_screen to ensure consistent snapshots across environments - Update snapshot with deterministic /test/workspace path instead of environment-specific path Co-authored-by: openhands <openhands@all-hands.dev> * Remove tests not related to OpenHands CLI's TUI - Remove TestSimpleWidgetSnapshots class (generic button grid test) - Remove integration tests directory (tests headless mode, not TUI) - Remove integration test fixtures from conftest.py - Update repo.md documentation Co-authored-by: openhands <openhands@all-hands.dev> * Simplify snapshot tests: keep only one real UI test Remove all mocked tests and keep only TestExitModalSnapshots.test_exit_modal_initial_state which tests the real ExitConfirmationModal widget without any mocks. Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 7bb0477 commit 2f7ca21

File tree

6 files changed

+349
-1
lines changed

6 files changed

+349
-1
lines changed

.openhands/microagents/repo.md

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,114 @@ If the user says something along the lines of "update the sha" or "update the ag
3838
4. Run `./build.sh` to confirm that the build still works
3939
5. Open a pull request with the changes
4040

41-
If the build fails, still open the pull request and explain what error you're seeing, and the steps you plan to take to fix it; don't fix it yet though.
41+
If the build fails, still open the pull request and explain what error you're seeing, and the steps you plan to take to fix it; don't fix it yet though.
42+
43+
## Snapshot Testing with pytest-textual-snapshot
44+
45+
The CLI uses [pytest-textual-snapshot](https://github.com/Textualize/pytest-textual-snapshot) for visual regression testing of Textual UI components. Snapshots are SVG screenshots that capture the exact visual state of the application.
46+
47+
### Running Snapshot Tests
48+
49+
```bash
50+
# Run all snapshot tests
51+
uv run pytest tests/snapshots/ -v
52+
53+
# Update snapshots when intentional UI changes are made
54+
uv run pytest tests/snapshots/ --snapshot-update
55+
```
56+
57+
### Snapshot Test Location
58+
59+
- **Test files**: `tests/snapshots/test_app_snapshots.py`
60+
- **Generated snapshots**: `tests/snapshots/__snapshots__/test_app_snapshots/*.svg`
61+
62+
### Writing Snapshot Tests
63+
64+
Snapshot tests must be **synchronous** (not async). The `snap_compare` fixture handles async internally:
65+
66+
```python
67+
from textual.app import App, ComposeResult
68+
from textual.widgets import Static, Footer
69+
70+
def test_my_widget(snap_compare):
71+
"""Snapshot test for my widget."""
72+
73+
class MyTestApp(App):
74+
def compose(self) -> ComposeResult:
75+
yield Static("Content")
76+
yield Footer()
77+
78+
assert snap_compare(MyTestApp(), terminal_size=(80, 24))
79+
```
80+
81+
#### Using `run_before` for Setup
82+
83+
To interact with the app before taking a screenshot:
84+
85+
```python
86+
def test_with_interaction(snap_compare):
87+
class MyApp(App):
88+
def compose(self) -> ComposeResult:
89+
yield InputField(id="input")
90+
91+
async def setup(pilot):
92+
input_field = pilot.app.query_one(InputField)
93+
input_field.input_widget.value = "Hello!"
94+
await pilot.pause()
95+
96+
assert snap_compare(MyApp(), terminal_size=(80, 24), run_before=setup)
97+
```
98+
99+
#### Using `press` for Key Simulation
100+
101+
```python
102+
def test_with_focus(snap_compare):
103+
assert snap_compare(
104+
MyApp(),
105+
terminal_size=(80, 24),
106+
press=["tab", "tab"], # Press tab twice to move focus
107+
)
108+
```
109+
110+
### Viewing Snapshots Visually
111+
112+
To view the generated SVG snapshots in a browser:
113+
114+
1. **Start a local HTTP server** in the snapshots directory:
115+
```bash
116+
cd tests/snapshots/__snapshots__/test_app_snapshots
117+
python -m http.server 12000
118+
```
119+
120+
2. **Open in browser** using the work host URL:
121+
```
122+
https://work-1-eidmcsndvfctphkv.prod-runtime.all-hands.dev/<snapshot-name>.svg
123+
```
124+
125+
Example snapshot names:
126+
- `TestExitModalSnapshots.test_exit_modal_initial_state.svg`
127+
- `TestOpenHandsAppSnapshots.test_openhands_app_splash_screen.svg`
128+
- `TestInputFieldSnapshots.test_input_field_with_text.svg`
129+
130+
3. **Stop the server** when done:
131+
```bash
132+
pkill -f "python -m http.server 12000"
133+
```
134+
135+
### Current Snapshot Tests
136+
137+
| Test Class | Test Name | Description |
138+
|------------|-----------|-------------|
139+
| `TestExitModalSnapshots` | `test_exit_modal_initial_state` | Exit confirmation modal initial view |
140+
| `TestExitModalSnapshots` | `test_exit_modal_with_focus_on_yes` | Exit modal with focus on Yes button |
141+
| `TestInputFieldSnapshots` | `test_input_field_single_line_mode` | Input field in default state |
142+
| `TestInputFieldSnapshots` | `test_input_field_with_text` | Input field with typed text |
143+
| `TestOpenHandsAppSnapshots` | `test_openhands_app_splash_screen` | Main app splash screen (mocked) |
144+
| `TestConfirmationModalSnapshots` | `test_confirmation_settings_modal` | Confirmation settings modal |
145+
146+
### Best Practices
147+
148+
1. **Mock external dependencies** - Use `unittest.mock.patch` to ensure deterministic snapshots
149+
2. **Use fixed terminal sizes** - Always specify `terminal_size=(width, height)` for consistent results
150+
3. **Commit snapshots to git** - SVG files are test artifacts and should be version controlled
151+
4. **Review snapshot diffs carefully** - When tests fail, examine the visual diff to determine if the change is intentional

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dev = [
4444
"pytest-cov>=5.0.0",
4545
"pytest-forked>=1.6.0",
4646
"pytest-xdist>=3.6.1",
47+
"pytest-textual-snapshot>=1.1.0",
4748
"ruff>=0.12.10",
4849
"pycodestyle>=2.12.0",
4950
"pyright[nodejs]>=1.1.405",
@@ -106,6 +107,9 @@ python_classes = ["Test*"]
106107
python_functions = ["test_*"]
107108
addopts = "-v --tb=short"
108109
asyncio_mode = "auto"
110+
markers = [
111+
"integration: marks tests as integration tests (deselect with '-m \"not integration\"')",
112+
]
109113

110114
[tool.coverage.run]
111115
relative_files = true

tests/snapshots/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Snapshot tests for OpenHands CLI using pytest-textual-snapshot."""

0 commit comments

Comments
 (0)