Skip to content

Commit 10341f4

Browse files
authored
Merge pull request #22 from kdkavanagh/fix-focus
Fix focus after search
2 parents e4936a8 + 65e9953 commit 10341f4

13 files changed

Lines changed: 393 additions & 745 deletions

File tree

.claude/skills/textual-testing.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
name: textual-testing
3+
description: Guide for testing and debugging Textual TUI applications
4+
---
5+
6+
# Testing & Debugging Textual TUI Apps
7+
8+
## Testing with pytest
9+
10+
### Setup
11+
- Use **pytest** with **pytest-asyncio**. Set `asyncio_mode = auto` in `pyproject.toml`.
12+
- All tests using `run_test()` must be `async def`.
13+
14+
### Running Apps in Tests: `App.run_test()`
15+
16+
Async context manager that runs the app headless (no terminal). Yields a `Pilot` object:
17+
18+
```python
19+
async def test_example():
20+
app = MyApp()
21+
async with app.run_test() as pilot:
22+
await pilot.press("r")
23+
assert app.screen.styles.background == Color.parse("red")
24+
```
25+
26+
Default terminal size is (80, 24). Override: `app.run_test(size=(100, 50))`.
27+
28+
### Pilot API — Simulating User Input
29+
30+
All methods are async.
31+
32+
**Key presses**`pilot.press(*keys)`:
33+
```python
34+
await pilot.press("h", "e", "l", "l", "o") # type text
35+
await pilot.press("enter") # special keys
36+
await pilot.press("ctrl+c") # modifier combos
37+
```
38+
39+
**Mouse clicks**`pilot.click(selector, offset, shift, meta, control, times)`:
40+
```python
41+
await pilot.click("#red") # CSS selector by ID
42+
await pilot.click(Button) # by widget class
43+
await pilot.click(Button, offset=(0, -1)) # with offset relative to widget
44+
await pilot.click(Button, times=2) # double-click
45+
```
46+
47+
**Other mouse**: `pilot.double_click()`, `pilot.triple_click()`, `pilot.hover()`, `pilot.mouse_down()`, `pilot.mouse_up()` — same signature as click.
48+
49+
**Resize**: `await pilot.resize_terminal(width=120, height=40)`
50+
51+
**Exit**: `await pilot.exit(result=some_value)`
52+
53+
**Wait for animations**:
54+
```python
55+
await pilot.wait_for_animation()
56+
await pilot.wait_for_scheduled_animations()
57+
```
58+
59+
### Managing Async Timing
60+
61+
Messages are processed asynchronously. Use `pilot.pause()` to wait for pending messages before asserting:
62+
63+
```python
64+
await pilot.pause() # wait for all pending messages
65+
await pilot.pause(delay=0.5) # delay then wait
66+
```
67+
68+
This is critical — without it, assertions may run before handlers complete.
69+
70+
### Asserting on State
71+
72+
```python
73+
assert app.screen.styles.background == Color.parse("red")
74+
assert app.query_one("#my-input", Input).value == "hello"
75+
assert app.focused is app.query_one("#my-button")
76+
```
77+
78+
Use `app.query()` or `app.query_one()` with CSS selectors to find widgets.
79+
80+
### Snapshot / Visual Regression Testing
81+
82+
Install `pytest-textual-snapshot`. Use the `snap_compare` fixture (sync, not async):
83+
84+
```python
85+
def test_calculator(snap_compare):
86+
assert snap_compare("path/to/calculator.py")
87+
```
88+
89+
First run always fails (no baseline). Run `pytest --snapshot-update` after visual verification.
90+
91+
Options: `press=["1", "2"]`, `terminal_size=(50, 100)`, `run_before=async_func`.
92+
93+
### Testing Best Practices
94+
95+
- Click targeting is realistic: overlaying widgets receive the click.
96+
- Use `pilot.pause()` liberally to avoid race conditions.
97+
- Snapshot tests are sync functions; `run_test()` tests are async.
98+
- Check Textual's own `tests/` directory on GitHub for advanced patterns.
99+
100+
## Debugging
101+
102+
### Dev Console (two terminals)
103+
104+
Terminal 1: `textual console`
105+
Terminal 2: `textual run --dev my_app.py`
106+
107+
The console shows `print()` output and internal logs. Verbosity: `-v` for verbose, `-x GROUP` to exclude groups (EVENT, DEBUG, INFO, WARNING, ERROR, PRINT, SYSTEM, LOGGING, WORKER).
108+
109+
### Logging
110+
111+
```python
112+
from textual import log
113+
114+
log("Hello") # simple string
115+
log(locals()) # variables
116+
log(children=self.children) # keyword args
117+
```
118+
119+
Or use `self.log()` on App/Widget instances. For stdlib logging:
120+
121+
```python
122+
import logging
123+
from textual.logging import TextualHandler
124+
logging.basicConfig(level="NOTSET", handlers=[TextualHandler()])
125+
```
126+
127+
### CSS Hot-Reload
128+
129+
`textual run --dev` enables live CSS reloading — edit `.tcss` files and see changes instantly.
130+
131+
### Browser-based
132+
133+
`textual serve my_app.py` converts the TUI into a web app viewable in a browser.

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: "3.12"
17+
- run: make activate
18+
- run: make check
19+
20+
test:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.12"
27+
- run: make activate
28+
- run: make test

0 commit comments

Comments
 (0)