Skip to main content

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}