iced_test/
lib.rs

1//! Test your `iced` applications in headless mode.
2//!
3//! # Basic Usage
4//! Let's assume we want to test [the classical counter interface].
5//!
6//! First, we will want to create a [`Simulator`] of our interface:
7//!
8//! ```rust,no_run
9//! # struct Counter { value: i64 }
10//! # impl Counter {
11//! #    pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
12//! # }
13//! use iced_test::simulator;
14//!
15//! let mut counter = Counter { value: 0 };
16//! let mut ui = simulator(counter.view());
17//! ```
18//!
19//! Now we can simulate a user interacting with our interface. Let's use [`Simulator::click`] to click
20//! the counter buttons:
21//!
22//! ```rust,no_run
23//! # struct Counter { value: i64 }
24//! # impl Counter {
25//! #    pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
26//! # }
27//! # use iced_test::simulator;
28//! #
29//! # let mut counter = Counter { value: 0 };
30//! # let mut ui = simulator(counter.view());
31//! #
32//! let _ = ui.click("+");
33//! let _ = ui.click("+");
34//! let _ = ui.click("-");
35//! ```
36//!
37//! [`Simulator::click`] takes a type implementing the [`Selector`] trait. A [`Selector`] describes a way to query the widgets of an interface.
38//! In this case, we leverage the [`Selector`] implementation of `&str`, which selects a widget by the text it contains.
39//!
40//! We can now process any messages produced by these interactions and then assert that the final value of our counter is
41//! indeed `1`!
42//!
43//! ```rust,no_run
44//! # struct Counter { value: i64 }
45//! # impl Counter {
46//! #    pub fn update(&mut self, message: ()) {}
47//! #    pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
48//! # }
49//! # use iced_test::simulator;
50//! #
51//! # let mut counter = Counter { value: 0 };
52//! # let mut ui = simulator(counter.view());
53//! #
54//! # let _ = ui.click("+");
55//! # let _ = ui.click("+");
56//! # let _ = ui.click("-");
57//! #
58//! for message in ui.into_messages() {
59//!     counter.update(message);
60//! }
61//!
62//! assert_eq!(counter.value, 1);
63//! ```
64//!
65//! We can even rebuild the interface to make sure the counter _displays_ the proper value with [`Simulator::find`]:
66//!
67//! ```rust,no_run
68//! # struct Counter { value: i64 }
69//! # impl Counter {
70//! #    pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
71//! # }
72//! # use iced_test::simulator;
73//! #
74//! # let mut counter = Counter { value: 0 };
75//! let mut ui = simulator(counter.view());
76//!
77//! assert!(ui.find("1").is_ok(), "Counter should display 1!");
78//! ```
79//!
80//! And that's it! That's the gist of testing `iced` applications!
81//!
82//! [`Simulator`] contains additional operations you can use to simulate more interactions—like [`tap_key`](Simulator::tap_key) or
83//! [`typewrite`](Simulator::typewrite)—and even perform [_snapshot testing_](Simulator::snapshot)!
84//!
85//! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface
86pub use iced_program as program;
87pub use iced_renderer as renderer;
88pub use iced_runtime as runtime;
89pub use iced_runtime::core;
90
91pub use iced_selector as selector;
92
93pub mod emulator;
94pub mod ice;
95pub mod instruction;
96pub mod simulator;
97
98mod error;
99
100pub use emulator::Emulator;
101pub use error::Error;
102pub use ice::Ice;
103pub use instruction::Instruction;
104pub use selector::Selector;
105pub use simulator::{Simulator, simulator};
106
107use std::path::Path;
108
109/// Runs an [`Ice`] test suite for the given [`Program`](program::Program).
110///
111/// Any `.ice` tests will be parsed from the given directory and executed in
112/// an [`Emulator`] of the given [`Program`](program::Program).
113///
114/// Remember that an [`Emulator`] executes the real thing! Side effects _will_
115/// take place. It is up to you to ensure your tests have reproducible environments
116/// by leveraging [`Preset`][program::Preset].
117pub fn run(
118    program: impl program::Program + 'static,
119    tests_dir: impl AsRef<Path>,
120) -> Result<(), Error> {
121    use crate::runtime::futures::futures::StreamExt;
122    use crate::runtime::futures::futures::channel::mpsc;
123    use crate::runtime::futures::futures::executor;
124
125    use std::ffi::OsStr;
126    use std::fs;
127
128    let files = fs::read_dir(tests_dir)?;
129    let mut tests = Vec::new();
130
131    for file in files {
132        let file = file?;
133
134        if file.path().extension().and_then(OsStr::to_str) != Some("ice") {
135            continue;
136        }
137
138        let content = fs::read_to_string(file.path())?;
139
140        match Ice::parse(&content) {
141            Ok(ice) => {
142                let preset = if let Some(preset) = &ice.preset {
143                    let Some(preset) = program
144                        .presets()
145                        .iter()
146                        .find(|candidate| candidate.name() == preset)
147                    else {
148                        return Err(Error::PresetNotFound {
149                            name: preset.to_owned(),
150                            available: program
151                                .presets()
152                                .iter()
153                                .map(program::Preset::name)
154                                .map(str::to_owned)
155                                .collect(),
156                        });
157                    };
158
159                    Some(preset)
160                } else {
161                    None
162                };
163
164                tests.push((file, ice, preset));
165            }
166            Err(error) => {
167                return Err(Error::IceParsingFailed {
168                    file: file.path().to_path_buf(),
169                    error,
170                });
171            }
172        }
173    }
174
175    // TODO: Concurrent runtimes
176    for (file, ice, preset) in tests {
177        let (sender, mut receiver) = mpsc::channel(1);
178
179        let mut emulator = Emulator::with_preset(
180            sender,
181            &program,
182            ice.mode,
183            ice.viewport,
184            preset,
185        );
186
187        let mut instructions = ice.instructions.into_iter();
188
189        loop {
190            let event = executor::block_on(receiver.next())
191                .expect("emulator runtime should never stop on its own");
192
193            match event {
194                emulator::Event::Action(action) => {
195                    emulator.perform(&program, action);
196                }
197                emulator::Event::Failed(instruction) => {
198                    return Err(Error::IceTestingFailed {
199                        file: file.path().to_path_buf(),
200                        instruction,
201                    });
202                }
203                emulator::Event::Ready => {
204                    let Some(instruction) = instructions.next() else {
205                        break;
206                    };
207
208                    emulator.run(&program, instruction);
209                }
210            }
211        }
212    }
213
214    Ok(())
215}