Skip to content

Commit 8399a04

Browse files
joskeclaude
andauthored
UI testing (#24)
* Add automated UI testing design document Three-layer strategy: pure Rust logic tests (CI), GTK widget state tests (CI + Xvfb), and Python/dogtail integration suite (pre-release). Targets 74% automation of TESTING.md checklist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add automated UI testing implementation plan 12-task plan covering: state model extraction (diff_state.rs, merge_state.rs), CLI integration tests, property-based tests, CI xvfb setup, dogtail pre-release suite, and test fixtures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add xvfb to CI for GTK widget tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Extract merge logic into merge_state.rs with conflict marker tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Extract navigation + label logic into diff_state.rs with tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add test fixtures and Makefile with test convenience targets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add CLI integration tests covering all launch mode error paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Extract search, gutter hit-test, chunk-map geometry into diff_state Add three pure functions to diff_state.rs: - find_all_matches: case-insensitive substring search with overlapping matches - hit_test_gutter_arrow: distance-based gutter arrow click detection - compute_chunk_map_rects: minimap sidebar rectangle computation Also adds GutterHit enum and ChunkMapRect struct, plus comprehensive tests covering edge cases for all three functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add dogtail test scaffold with smoke test for file diff Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add manual-dispatch UI integration test workflow for pre-release Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add unit tests for generate_unified_diff and read_file_content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add property-based tests for navigation and unified diff format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add dogtail keyboard shortcut tests for Ctrl+F and Alt+Down Tests verify: - Ctrl+F opens the find bar with a text entry - Alt+Down navigates to the next diff chunk and updates the label - Escape closes the find bar after opening Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Expand dogtail tests: directory comparison, VCS window, save button state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Wire common.rs and merge_view.rs to delegate to extracted state modules - common.rs: Side enum re-exported from diff_state, navigate_chunk uses diff_state::find_next_chunk, update_chunk_label uses diff_state::format_chunk_label, handle_gutter_click uses diff_state::hit_test_gutter_arrow, draw_chunk_map uses diff_state::compute_chunk_map_rects - merge_view.rs: merge_change_indices and find_conflict_markers delegate to merge_state equivalents, duplicate tests removed - Net -176 lines of duplicated logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use venv for Python test dependencies Avoids PEP 668 "externally-managed-environment" errors on modern distros. `make test-release` now creates a venv automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use --system-site-packages for venv (dogtail needs gi) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix dogtail tests: add retry helper, fix API compat, add cleanup - conftest.py: add find_app() with retry loop and doDelay for AT-SPI tree population, increase launch wait to 3s, add post-teardown sleep - Remove showingOnly kwarg (not in current dogtail API) - Broaden dir tree view role search for GTK4 ColumnView - Add __pycache__/ to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix shortcut tests: grab focus before sending key events dogtail.rawinput sends keys via xdotool to the focused window. Under xvfb the mergers window doesn't have focus by default, so keys leaked to the terminal. Fix by calling grabFocus() on the frame first. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix chunk navigation test: click button instead of simulating Alt+Down dogtail.rawinput.keyCombo("<Alt>Down") sends terminal escape sequences instead of X11 key events to the GTK window. Use AT-SPI button click on the "Next change" button which is reliable under xvfb. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use xdotool with window targeting for keyboard shortcut tests dogtail.rawinput.keyCombo sends terminal escape sequences instead of X11 key events. Use xdotool directly with --window flag to target the mergers window. This properly tests the actual keyboard shortcuts rather than clicking buttons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use Ctrl+D for chunk navigation test instead of Alt+Down Alt+Down is handled in a capture-phase EventControllerKey on the text views, so it only fires when a text view has keyboard focus. Ctrl+D is registered app-level via set_accels_for_action and works from any focus. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix _send_keys: focus window before sending key GTK accelerators require the window to hold X11 focus; sending a key with --window alone bypasses the focus requirement. Use windowfocus --sync first, then key without --window. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix _send_keys: search by PID, skip non-focusable child windows xdotool search --name returns child GdkSurfaces that raise BadMatch on windowfocus. Search by --pid instead, and try each window until one accepts focus (top-level frames do, child surfaces don't). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add Testing section to README Describes the two-layer testing strategy: pure Rust unit tests and dogtail/AT-SPI UI integration tests under xvfb. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d69fe60 commit 8399a04

32 files changed

Lines changed: 3337 additions & 217 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@v6
1414
- name: Install additional packages
15-
run: sudo apt-get update && sudo apt-get install -y build-essential libgtksourceview-5-dev
15+
run: sudo apt-get update && sudo apt-get install -y build-essential libgtksourceview-5-dev xvfb fonts-dejavu-core
1616
- name: Install Rust
1717
uses: actions-rust-lang/setup-rust-toolchain@v1
1818
with:
@@ -24,4 +24,7 @@ jobs:
2424
- name: Clippy
2525
run: cargo clippy --all-targets -- -D warnings -W clippy::pedantic
2626
- name: Run tests
27-
run: cargo test --all-targets --locked
27+
run: xvfb-run -a cargo test --all-targets --locked
28+
env:
29+
GDK_BACKEND: x11
30+
GTK_A11Y: none

.github/workflows/ui-tests.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: UI Integration Tests
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
ui-tests:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v6
11+
12+
- name: Install system dependencies
13+
run: |
14+
sudo apt-get update
15+
sudo apt-get install -y \
16+
libgtksourceview-5-dev \
17+
xvfb \
18+
fonts-dejavu-core \
19+
at-spi2-core \
20+
python3-pip \
21+
python3-gi \
22+
gir1.2-atspi-2.0
23+
24+
- name: Install Rust toolchain
25+
uses: actions-rust-lang/setup-rust-toolchain@v1
26+
27+
- name: Cache cargo
28+
uses: Swatinem/rust-cache@v2
29+
30+
- name: Build release
31+
run: cargo build --release
32+
33+
- name: Install Python dependencies
34+
run: |
35+
python3 -m venv --system-site-packages tests/ui_integration/.venv
36+
tests/ui_integration/.venv/bin/pip install -r tests/ui_integration/requirements.txt
37+
38+
- name: Run Rust tests under Xvfb
39+
env:
40+
GDK_BACKEND: x11
41+
GTK_A11Y: none
42+
run: xvfb-run -a cargo test --all-targets --locked
43+
44+
- name: Run dogtail integration tests
45+
env:
46+
GDK_BACKEND: x11
47+
run: |
48+
eval $(dbus-launch --sh-syntax)
49+
/usr/libexec/at-spi2-registryd &
50+
sleep 1
51+
xvfb-run -a tests/ui_integration/.venv/bin/python3 -m pytest tests/ui_integration/ -v --tb=long

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ target/
1111
# Environment
1212
.env
1313
.env.*
14+
.venv/
15+
__pycache__/
1416

1517
# OS
1618
.DS_Store

Makefile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.PHONY: test test-ui test-release fmt clippy check
2+
3+
VENV := tests/ui_integration/.venv
4+
PYTEST := $(VENV)/bin/python3 -m pytest
5+
6+
test:
7+
cargo test
8+
9+
test-ui:
10+
xvfb-run -a cargo test
11+
12+
$(VENV): tests/ui_integration/requirements.txt
13+
python3 -m venv --system-site-packages $(VENV)
14+
$(VENV)/bin/pip install -r tests/ui_integration/requirements.txt
15+
@touch $(VENV)
16+
17+
test-release: $(VENV)
18+
xvfb-run -a cargo test
19+
xvfb-run -a $(PYTEST) tests/ui_integration/ -v
20+
21+
fmt:
22+
cargo +nightly fmt
23+
24+
clippy:
25+
cargo clippy -- -W clippy::pedantic
26+
27+
check: fmt clippy test

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,45 @@ brew install gtk4 gtksourceview5
156156
cargo build --release
157157
```
158158

159+
## Testing
160+
161+
mergers uses a three-layer testing strategy:
162+
163+
**Layer 1 — Pure unit tests (Rust)**
164+
165+
Core algorithms (Myers diff, chunk navigation, merge logic, gutter hit-testing, chunk-map geometry) are extracted into pure functions in `src/ui/diff_state.rs` and `src/ui/merge_state.rs` and covered by `cargo test`. These run headlessly on every CI push — no display server needed.
166+
167+
```bash
168+
cargo test
169+
```
170+
171+
**Layer 2 — UI integration tests (dogtail / AT-SPI)**
172+
173+
Higher-level tests drive the real GTK4 application via the AT-SPI accessibility bus using [dogtail](https://gitlab.gnome.org/GNOME/dogtail). They exercise file comparison, directory comparison, VCS mode, and keyboard shortcuts end-to-end. An X virtual framebuffer (Xvfb) is used so tests run headlessly in CI.
174+
175+
Test files live in `tests/ui_integration/`. Dependencies are managed in a local venv:
176+
177+
```bash
178+
# First time — sets up the venv and installs dogtail
179+
make test-release
180+
181+
# Subsequent runs
182+
make test-release
183+
```
184+
185+
`make test-release` runs `cargo test` followed by `pytest tests/ui_integration/ -v` under `xvfb-run`.
186+
187+
**Running UI tests manually (with a display)**
188+
189+
```bash
190+
source tests/ui_integration/.venv/bin/activate
191+
pytest tests/ui_integration/ -v
192+
```
193+
194+
**CI**
195+
196+
The `.github/workflows/ui-tests.yml` workflow runs the full suite on every push, installing `at-spi2-core` and `xdotool` from the system package manager before invoking `make test-release`.
197+
159198
## License
160199

161200
GPL-2.0
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Automated UI Testing Design
2+
3+
## Goal
4+
5+
Minimize manual UI testing by automating ~74% of TESTING.md checklist items across three layers: pure Rust unit tests, GTK widget state tests, and a Python/dogtail pre-release integration suite.
6+
7+
## Architecture: Three-Layer Testing Strategy
8+
9+
### Layer 1: Pure Rust Unit Tests (every CI run, no display)
10+
11+
Extract pure logic from GTK callbacks into standalone testable functions. Add CLI integration tests and property-based tests.
12+
13+
**State model extraction** — new files `diff_state.rs` and `merge_state.rs`:
14+
15+
| New pure function | Extracted from | Source location |
16+
|---|---|---|
17+
| `find_next_chunk(chunks, cursor_line, direction, side)` | `navigate_chunk` | common.rs:1567 |
18+
| `format_chunk_label(total, current)` | `update_chunk_label` | common.rs:1645 |
19+
| `find_conflict_markers_in_text(text)` | `find_conflict_markers` | merge_view.rs:181 |
20+
| `hit_test_gutter_arrows(x, y, width, ...)` | `handle_gutter_click` | common.rs:1358 |
21+
| `compute_chunk_map_rects(chunks, height, total_lines)` | `draw_chunk_map` | common.rs:1475 |
22+
| `plan_copy_chunk(chunks, idx, direction, left_text, right_text)` | `copy_chunk` | common.rs:1265 |
23+
| `find_all_matches(text, needle)` | `highlight_search_matches` | common.rs:1424 |
24+
| `swap_state(left, right)` involution | swap callbacks | various |
25+
26+
**CLI integration tests** (`tests/cli_integration.rs`):
27+
- Test all 15 CLI/Launch Mode items via `std::process::Command`
28+
- Assert exit codes and stderr output for each argument combination
29+
30+
**Extended property-based tests**:
31+
- Unified diff format validity roundtrip
32+
- Navigation invariants (next chunk always past cursor)
33+
- `filter_for_diff` + `remap_chunks` roundtrip
34+
35+
### Layer 2: `#[gtk::test]` Widget State Tests (every CI run, needs Xvfb)
36+
37+
Create GTK widgets programmatically and verify their state:
38+
39+
- `build_diff_view` with test data: verify tag placement, button sensitivity, action groups
40+
- `build_merge_view` with 3-way data: verify pane read-only state, conflict overlay
41+
- TextBuffer manipulation: verify save button goes sensitive on edit
42+
- Keybinding registration: verify action names exist in action groups
43+
- Identical/binary file detection: verify info bar creation, button disabled states
44+
45+
**File**: `tests/widget_tests.rs`
46+
47+
### Layer 3: Dogtail + Screenshot Suite (pre-release only)
48+
49+
**Dogtail** (Python AT-SPI) tests in `tests/ui_integration/`:
50+
- File diff workflow: open files, verify chunk count, navigate, copy chunk, save
51+
- Directory comparison: open dir, expand tree, double-click file, verify tab
52+
- 3-way merge: copy from left/right gutter, verify middle buffer
53+
- VCS window: open repo, double-click file, verify diff tab
54+
- Keyboard shortcuts: Ctrl+F, Ctrl+S, Alt+Up/Down, Ctrl+J/K, etc.
55+
56+
**Screenshot regression**:
57+
- Capture views via `WidgetPaintable` -> `Renderer` -> `Texture` -> PNG
58+
- Compare against reference images using `image-compare` crate (SSIM)
59+
- Fuzzy threshold to tolerate font/rendering differences
60+
61+
## Project Structure
62+
63+
```
64+
src/ui/
65+
diff_state.rs # NEW - pure diff logic (extracted from common.rs/diff_view.rs)
66+
merge_state.rs # NEW - pure merge logic (extracted from merge_view.rs)
67+
common.rs # MODIFIED - extracted fns become thin wrappers
68+
diff_view.rs # MODIFIED - delegates to diff_state
69+
merge_view.rs # MODIFIED - delegates to merge_state
70+
71+
tests/
72+
vcs_integration.rs # existing
73+
cli_integration.rs # NEW - CLI argument tests
74+
widget_tests.rs # NEW - #[gtk::test] widget state tests
75+
76+
tests/ui_integration/ # NEW - Python dogtail suite
77+
conftest.py # pytest fixtures (launch app, AT-SPI connection)
78+
test_file_diff.py
79+
test_dir_compare.py
80+
test_merge.py
81+
test_vcs.py
82+
test_shortcuts.py
83+
fixtures/ # test data files
84+
left.txt / right.txt / base.txt
85+
left_dir/ / right_dir/
86+
binary_file.bin
87+
identical_a.txt / identical_b.txt
88+
89+
tests/screenshots/ # NEW - visual regression
90+
refs/ # reference PNGs
91+
```
92+
93+
## CI Changes
94+
95+
### Existing CI (`.github/workflows/ci.yml`)
96+
97+
Add xvfb and font packages for widget tests:
98+
99+
```yaml
100+
- name: Install packages
101+
run: sudo apt-get update && sudo apt-get install -y
102+
build-essential libgtksourceview-5-dev xvfb fonts-dejavu-core
103+
104+
- name: Run tests
105+
env:
106+
GDK_BACKEND: x11
107+
GTK_A11Y: none
108+
run: xvfb-run -a cargo test --all-targets --locked
109+
```
110+
111+
### New pre-release workflow (`.github/workflows/ui-tests.yml`)
112+
113+
Manual dispatch for pre-release validation:
114+
115+
```yaml
116+
- Install: xvfb, fonts, python3, pip, at-spi2-registryd, dogtail
117+
- Build: cargo build --release
118+
- Run: xvfb-run pytest tests/ui_integration/ -v
119+
- Run: xvfb-run cargo test --test screenshot_tests
120+
- Upload: screenshot diff artifacts on failure
121+
```
122+
123+
### Convenience targets (`Makefile`)
124+
125+
```makefile
126+
test: cargo test
127+
test-ui: xvfb-run -a cargo test
128+
test-release: xvfb-run -a pytest tests/ui_integration/ && xvfb-run -a cargo test --test screenshot_tests
129+
```
130+
131+
## Coverage Map
132+
133+
| Section | Items | Automated | Rate |
134+
|---|---|---|---|
135+
| CLI / Launch Modes | 15 | 15 | 100% |
136+
| Welcome Window | 6 | 3 | 50% |
137+
| File Diff (all subsections) | 45 | 35 | 78% |
138+
| Navigation | 4 | 4 | 100% |
139+
| Directory Comparison | 30 | 18 | 60% |
140+
| 3-Way Merge | 20 | 13 | 65% |
141+
| VCS Window | 15 | 11 | 73% |
142+
| Preferences | 12 | 6 | 50% |
143+
| Global Shortcuts | 20 | 18 | 90% |
144+
| **Total** | **~167** | **~123** | **~74%** |
145+
146+
### Items that remain manual (~26%)
147+
148+
These are genuinely hard to automate in the GTK4 ecosystem:
149+
- **File dialog interactions**: OS-native portal dialogs, can't drive from within process
150+
- **Visual rendering judgment**: Chunk background colors, gutter curve aesthetics, filler line positions, inline highlight colors
151+
- **Timing-dependent behavior**: File watcher debouncing (500ms poll), save suppression (600ms window), background diff responsiveness
152+
- **Cross-platform specifics**: macOS dark mode detection, Windows build behavior
153+
- **Scroll smoothness**: Subjective UX quality that requires human judgment
154+
155+
## Key Dependencies
156+
157+
### Rust (dev-dependencies)
158+
- `proptest` (existing) - property-based testing
159+
- `tempfile` (existing) - temporary files/dirs
160+
- `image-compare` (new) - SSIM screenshot comparison
161+
- `image` (new) - PNG encoding/decoding
162+
163+
### Python (pre-release only)
164+
- `dogtail` >= 1.0 - AT-SPI UI testing framework
165+
- `pytest` - test runner
166+
167+
### System (CI)
168+
- `xvfb` - virtual X11 display
169+
- `fonts-dejavu-core` - consistent font rendering
170+
- `at-spi2-registryd` - accessibility bus (for dogtail)
171+
172+
## Implementation Order
173+
174+
1. **Extract pure logic** into `diff_state.rs` / `merge_state.rs` + unit tests
175+
2. **CLI integration tests** in `tests/cli_integration.rs`
176+
3. **Property-based test extensions** for unified diff, navigation, filter/remap
177+
4. **CI xvfb setup** + `#[gtk::test]` widget state tests
178+
5. **Dogtail test suite** scaffold + fixture files
179+
6. **Screenshot regression** harness + initial reference images
180+
7. **Makefile** + pre-release workflow

0 commit comments

Comments
 (0)