|
| 1 | +# Integration Tests Guide |
| 2 | + |
| 3 | +This directory contains integration tests for `iced_aw` widgets using the `iced_test` simulator framework. |
| 4 | + |
| 5 | +## Quick Start |
| 6 | + |
| 7 | +It is suggested that integration test writers try to use the common test helpers to avoid code duplication: |
| 8 | + |
| 9 | +```rust |
| 10 | +//! Integration tests for the MyWidget widget |
| 11 | + |
| 12 | +#[macro_use] |
| 13 | +mod common; |
| 14 | + |
| 15 | +use iced::Element; |
| 16 | +use iced_aw::MyWidget; |
| 17 | + |
| 18 | +#[derive(Clone, Debug)] |
| 19 | +enum Message { |
| 20 | + MyAction, |
| 21 | +} |
| 22 | + |
| 23 | +// Generate test helpers for your Message type |
| 24 | +test_helpers!(Message); |
| 25 | + |
| 26 | +#[test] |
| 27 | +fn my_widget_renders() { |
| 28 | + run_test(|| MyWidget::new().into()); |
| 29 | +} |
| 30 | + |
| 31 | +#[test] |
| 32 | +fn my_widget_shows_text() { |
| 33 | + run_test_and_find(|| MyWidget::new().into(), "Expected Text"); |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +## Available Helpers |
| 38 | + |
| 39 | +The `test_helpers!` macro generates the following for your Message type: |
| 40 | + |
| 41 | +### Types |
| 42 | +- **`App`** - Test application wrapper with `new()`, `update()`, and `view()` methods |
| 43 | +- **`ViewFn`** - Type alias for view functions: `Box<dyn Fn() -> Element<'static, Message>>` |
| 44 | + |
| 45 | +### Functions |
| 46 | +- **`simulator(app: &App) -> Simulator<'_, Message>`** |
| 47 | + Creates a simulator with default settings |
| 48 | + |
| 49 | +- **`run_test<F>(view_fn: F)`** |
| 50 | + Verifies a widget renders without panicking |
| 51 | + ```rust |
| 52 | + run_test(|| MyWidget::new().into()); |
| 53 | + ``` |
| 54 | + |
| 55 | +- **`run_test_and_find<F>(view_fn: F, text: &str)`** |
| 56 | + Verifies a widget renders and contains specific text |
| 57 | + ```rust |
| 58 | + run_test_and_find(|| MyWidget::new().into(), "Button Label"); |
| 59 | + ``` |
| 60 | + |
| 61 | +## Advanced Usage |
| 62 | + |
| 63 | +### Stateful Tests |
| 64 | + |
| 65 | +For tests requiring state management, it is recommended to use `App` directly: |
| 66 | + |
| 67 | +```rust |
| 68 | +#[test] |
| 69 | +fn button_click_produces_message() -> Result<(), Error> { |
| 70 | + let (mut app, _) = App::new(|| MyWidget::new().into()); |
| 71 | + let mut ui = simulator(&app); |
| 72 | + |
| 73 | + ui.click("Button")?; |
| 74 | + |
| 75 | + for message in ui.into_messages() { |
| 76 | + app.update(message); |
| 77 | + } |
| 78 | + |
| 79 | + Ok(()) |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +### Custom Stateful Apps |
| 84 | + |
| 85 | +For complex state tracking, it is recommended to define a custom app alongside the generated helpers: |
| 86 | + |
| 87 | +```rust |
| 88 | +#[derive(Clone)] |
| 89 | +struct StatefulTestApp { |
| 90 | + counter: std::rc::Rc<std::cell::RefCell<usize>>, |
| 91 | +} |
| 92 | + |
| 93 | +impl StatefulTestApp { |
| 94 | + fn new() -> (Self, iced::Task<Message>) { |
| 95 | + (Self { counter: std::rc::Rc::new(std::cell::RefCell::new(0)) }, iced::Task::none()) |
| 96 | + } |
| 97 | + |
| 98 | + fn update(&mut self, message: Message) { |
| 99 | + match message { |
| 100 | + Message::Increment => *self.counter.borrow_mut() += 1, |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + fn view(&self) -> Element<'_, Message> { |
| 105 | + MyWidget::new(*self.counter.borrow()).into() |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +## Test Organization |
| 111 | + |
| 112 | +For consistency, it is suggested that you structure your tests with clear section comments: |
| 113 | + |
| 114 | +```rust |
| 115 | +// ============================================================================ |
| 116 | +// Basic Rendering Tests |
| 117 | +// ============================================================================ |
| 118 | + |
| 119 | +#[test] |
| 120 | +fn widget_renders_with_defaults() { /* ... */ } |
| 121 | + |
| 122 | +// ============================================================================ |
| 123 | +// Interaction Tests |
| 124 | +// ============================================================================ |
| 125 | + |
| 126 | +#[test] |
| 127 | +fn button_click_works() { /* ... */ } |
| 128 | +``` |
| 129 | + |
| 130 | +## Simulator API |
| 131 | + |
| 132 | +Full API documentation: https://github.com/iced-rs/iced/blob/master/test/src/simulator.rs |
| 133 | + |
| 134 | +Key methods: |
| 135 | +- `ui.find(text)` - Locate text in the widget tree |
| 136 | +- `ui.click(text)` - Click on element containing text |
| 137 | +- `ui.tap_key(key)` - Simulate keyboard input |
| 138 | +- `ui.into_messages()` - Consume simulator and get produced messages |
| 139 | + |
| 140 | +## Suggested Best Practices |
| 141 | + |
| 142 | +1. **Use `run_test_and_find` when possible** - Tests both rendering and `operate()` implementation |
| 143 | +2. **Keep test names descriptive** - Name should explain what's being tested |
| 144 | +3. **Test one thing per test** - Avoid complex multi-step tests |
| 145 | +4. **Use `Result<(), Error>` for tests with `?` operator** - Makes error handling cleaner |
| 146 | +5. **Keep imports minimal** - Only import what you actually use |
| 147 | + |
| 148 | +## Test Naming Convention |
| 149 | + |
| 150 | +It is suggested that integration test function names begin with the widget name as a prefix. |
| 151 | + |
| 152 | +### Naming Pattern |
| 153 | + |
| 154 | +```rust |
| 155 | +fn <widget_name>_<test_description>() |
| 156 | +``` |
| 157 | + |
| 158 | +### Example test names |
| 159 | + |
| 160 | +**Correct:** |
| 161 | +```rust |
| 162 | +#[test] |
| 163 | +fn badge_renders_with_text() { /* ... */ } |
| 164 | + |
| 165 | +#[test] |
| 166 | +fn number_input_increment_button_click_produces_message() { /* ... */ } |
| 167 | + |
| 168 | +#[test] |
| 169 | +fn time_picker_displays_12h_format_correctly() { /* ... */ } |
| 170 | +``` |
| 171 | + |
| 172 | +**Incorrect:** |
| 173 | +```rust |
| 174 | +#[test] |
| 175 | +fn renders_with_text() { /* ... */ } // Missing widget prefix |
| 176 | + |
| 177 | +#[test] |
| 178 | +fn can_increment_number() { /* ... */ } // Missing widget prefix |
| 179 | + |
| 180 | +#[test] |
| 181 | +fn displays_time() { /* ... */ } // Missing widget prefix |
| 182 | +``` |
| 183 | + |
| 184 | +This convention helps ensure: |
| 185 | +- Tests are clearly associated with their widget |
| 186 | +- Test output is easier to read and filter |
| 187 | +- grep/search operations are more effective |
| 188 | + |
| 189 | +## Example Test File Structure |
| 190 | + |
| 191 | +```rust |
| 192 | +//! Integration tests for the Widget |
| 193 | +//! |
| 194 | +//! Brief description of what these tests cover. |
| 195 | + |
| 196 | +#[macro_use] |
| 197 | +mod common; |
| 198 | + |
| 199 | +use iced::{Element, Length}; |
| 200 | +use iced_aw::Widget; |
| 201 | +use iced_test::{Error, Simulator}; |
| 202 | + |
| 203 | +#[derive(Clone, Debug)] |
| 204 | +enum Message { |
| 205 | + Action, |
| 206 | +} |
| 207 | + |
| 208 | +test_helpers!(Message); |
| 209 | + |
| 210 | +// ============================================================================ |
| 211 | +// Basic Tests |
| 212 | +// ============================================================================ |
| 213 | + |
| 214 | +#[test] |
| 215 | +fn widget_renders() { |
| 216 | + run_test(|| Widget::new().into()); |
| 217 | +} |
| 218 | + |
| 219 | +#[test] |
| 220 | +fn widget_with_text() { |
| 221 | + run_test_and_find(|| Widget::new().with_label("Test").into(), "Test"); |
| 222 | +} |
| 223 | + |
| 224 | +// ============================================================================ |
| 225 | +// Configuration Tests |
| 226 | +// ============================================================================ |
| 227 | + |
| 228 | +#[test] |
| 229 | +fn widget_with_custom_width() { |
| 230 | + run_test(|| Widget::new().width(Length::Fill).into()); |
| 231 | +} |
| 232 | + |
| 233 | +// ============================================================================ |
| 234 | +// Interaction Tests |
| 235 | +// ============================================================================ |
| 236 | + |
| 237 | +#[test] |
| 238 | +fn widget_click_works() -> Result<(), Error> { |
| 239 | + let (mut app, _) = App::new(|| Widget::new().into()); |
| 240 | + let mut ui = simulator(&app); |
| 241 | + |
| 242 | + ui.click("Target")?; |
| 243 | + |
| 244 | + let mut got_message = false; |
| 245 | + for message in ui.into_messages() { |
| 246 | + got_message = true; |
| 247 | + app.update(message); |
| 248 | + } |
| 249 | + |
| 250 | + assert!(got_message); |
| 251 | + Ok(()) |
| 252 | +} |
| 253 | +``` |
| 254 | +## Button Icons Reference |
| 255 | + |
| 256 | +Common icon characters used in widgets (searchable with `ui.find()`): |
| 257 | + |
| 258 | +- `\u{e800}` - cancel (close/cancel buttons) |
| 259 | +- `\u{e801}` - down_open (down arrow, dropdowns) |
| 260 | +- `\u{e802}` - left_open (left navigation) |
| 261 | +- `\u{e803}` - right_open (right navigation) |
| 262 | +- `\u{e804}` - up_open (up arrow, increment) |
| 263 | +- `\u{e805}` - ok (checkmark/submit) |
| 264 | + |
| 265 | +Or search for their display forms: `" ▲ "`, `" ▼ "`, `" ✓ "`, etc. |
0 commit comments