revue/testing/mod.rs
1//! Pilot testing framework for automated UI tests.
2//!
3//! Test your TUI applications with simulated user interactions,
4//! assertions on rendered output, and snapshot testing.
5//!
6//! # Features
7//!
8//! | Feature | Description |
9//! |---------|-------------|
10//! | **Key Simulation** | Simulate keyboard input |
11//! | **Text Search** | Assert on rendered text |
12//! | **Snapshot Testing** | Compare against golden files |
13//! | **Visual Regression** | Color & style comparison |
14//! | **CI Integration** | GitHub Actions, GitLab CI |
15//! | **Async Support** | Test async operations |
16//! | **Action Sequences** | Chain multiple actions |
17//!
18//! # Quick Start
19//!
20//! ```rust,ignore
21//! use revue::testing::{Pilot, TestApp};
22//! use revue::event::Key;
23//!
24//! #[test]
25//! fn test_counter() {
26//! let mut app = TestApp::new(Counter::new());
27//! let mut pilot = Pilot::new(&mut app);
28//!
29//! pilot
30//! .press(Key::Up)
31//! .press(Key::Up)
32//! .assert_contains("Count: 2")
33//! .snapshot("counter_at_2");
34//! }
35//! ```
36//!
37//! # Pilot API
38//!
39//! ## Key Simulation
40//!
41//! ```rust,ignore
42//! pilot
43//! .press(Key::Enter) // Press Enter
44//! .press(Key::Char('a')) // Type 'a'
45//! .type_text("hello") // Type string
46//! .press(Key::Escape); // Press Escape
47//! ```
48//!
49//! ## Assertions
50//!
51//! ```rust,ignore
52//! pilot
53//! .assert_contains("Welcome") // Text exists
54//! .assert_not_contains("Error") // Text doesn't exist
55//! .assert_focused(".input") // Element is focused
56//! .assert_visible(".modal"); // Element is visible
57//! ```
58//!
59//! ## Snapshot Testing
60//!
61//! ```rust,ignore
62//! pilot
63//! .snapshot("initial_state") // Save/compare snapshot
64//! .press(Key::Enter)
65//! .snapshot("after_enter");
66//! ```
67//!
68//! Snapshots are stored in `tests/snapshots/` and can be updated with:
69//! ```bash
70//! REVUE_UPDATE_SNAPSHOTS=1 cargo test
71//! ```
72//!
73//! ## Waiting
74//!
75//! ```rust,ignore
76//! pilot
77//! .wait_ms(100) // Wait 100ms
78//! .wait_until(|screen| { // Wait for condition
79//! screen.contains("Loaded")
80//! });
81//! ```
82//!
83//! # TestApp
84//!
85//! [`TestApp`] wraps your view for testing:
86//!
87//! ```rust,ignore
88//! use revue::testing::{TestApp, TestConfig};
89//!
90//! // Default 80x24 terminal
91//! let app = TestApp::new(MyView::new());
92//!
93//! // Custom size
94//! let config = TestConfig {
95//! width: 120,
96//! height: 40,
97//! ..Default::default()
98//! };
99//! let app = TestApp::with_config(MyView::new(), config);
100//! ```
101//!
102//! # Action Sequences
103//!
104//! Build reusable action sequences:
105//!
106//! ```rust,ignore
107//! use revue::testing::{ActionSequence, Action};
108//!
109//! let login_sequence = ActionSequence::new()
110//! .action(Action::Type("admin".into()))
111//! .action(Action::Press(Key::Tab))
112//! .action(Action::Type("password".into()))
113//! .action(Action::Press(Key::Enter));
114//!
115//! pilot.run_sequence(&login_sequence);
116//! ```
117//!
118//! # Async Testing
119//!
120//! For testing async operations:
121//!
122//! ```rust,ignore
123//! use revue::testing::AsyncPilot;
124//!
125//! #[tokio::test]
126//! async fn test_async_load() {
127//! let app = TestApp::new(MyAsyncView::new());
128//!
129//! AsyncPilot::run(app, |pilot| async move {
130//! pilot.press(Key::Enter).await;
131//! pilot.wait_until_async(|s| s.contains("Loaded")).await;
132//! pilot.assert_contains("Data loaded");
133//! }).await;
134//! }
135//! ```
136//!
137//! # Visual Regression Testing
138//!
139//! For pixel-perfect UI testing with color and style comparison:
140//!
141//! ```rust,ignore
142//! use revue::testing::{VisualTest, VisualTestConfig};
143//!
144//! #[test]
145//! fn test_button_styles() {
146//! let test = VisualTest::new("button_normal")
147//! .group("buttons");
148//!
149//! let buffer = render_my_widget();
150//! test.assert_matches(&buffer);
151//! }
152//! ```
153//!
154//! Update golden files:
155//! ```bash
156//! REVUE_UPDATE_VISUALS=1 cargo test
157//! ```
158//!
159//! # CI Integration
160//!
161//! Detect CI environments and generate reports:
162//!
163//! ```rust,ignore
164//! use revue::testing::{CiEnvironment, TestReport};
165//!
166//! let ci = CiEnvironment::detect();
167//! let mut report = TestReport::new();
168//!
169//! // Run tests...
170//! report.add_passed("button_test");
171//! report.add_failed("modal_test", "Size mismatch");
172//!
173//! // Generate CI-specific output
174//! report.write_summary(&ci);
175//! report.save_artifacts(&ci).ok();
176//! ```
177//!
178//! # Best Practices
179//!
180//! 1. **Name snapshots descriptively**: `"login_form_with_error"` not `"test1"`
181//! 2. **Test user flows**: Simulate realistic user interactions
182//! 3. **Keep tests focused**: One behavior per test
183//! 4. **Use wait_until for async**: Don't rely on fixed delays
184//! 5. **Use visual tests for styling**: Catch color and layout regressions
185//! 6. **Run in CI**: Use `CiEnvironment` for portable test reports
186
187mod actions;
188mod assertions;
189pub mod ci;
190pub mod mock;
191mod pilot;
192mod snapshot;
193mod test_app;
194pub mod visual;
195
196pub use actions::{Action, ActionSequence, KeyAction, MouseAction};
197pub use assertions::{Assertion, AssertionResult};
198pub use mock::{
199 capture_render, mock_alt_key, mock_click, mock_ctrl_key, mock_key, mock_mouse, mock_terminal,
200 mock_time, simulate_user, EventSimulator, MockState, MockTerminal, MockTime, RenderCapture,
201 SimulatedEvent,
202};
203pub use pilot::{AsyncPilot, Pilot};
204pub use snapshot::SnapshotManager;
205pub use test_app::TestApp;
206
207// Visual regression testing
208pub use visual::{
209 CapturedCell, CellDiff, VisualCapture, VisualDiff, VisualTest, VisualTestConfig,
210 VisualTestResult,
211};
212
213// CI integration
214pub use ci::{CiEnvironment, CiProvider, TestReport, TestResult};
215
216/// Test configuration
217#[derive(Debug, Clone)]
218pub struct TestConfig {
219 /// Screen width
220 pub width: u16,
221 /// Screen height
222 pub height: u16,
223 /// Timeout for async operations (ms)
224 pub timeout_ms: u64,
225 /// Enable debug output
226 pub debug: bool,
227}
228
229impl Default for TestConfig {
230 fn default() -> Self {
231 Self {
232 width: 80,
233 height: 24,
234 timeout_ms: 5000,
235 debug: false,
236 }
237 }
238}
239
240impl TestConfig {
241 /// Create with custom size
242 pub fn with_size(width: u16, height: u16) -> Self {
243 Self {
244 width,
245 height,
246 ..Default::default()
247 }
248 }
249}