1use crate::core;
3use crate::core::event;
4use crate::core::keyboard;
5use crate::core::mouse;
6use crate::core::theme;
7use crate::core::time;
8use crate::core::widget;
9use crate::core::window;
10use crate::core::{Element, Event, Point, Settings, Size, SmolStr};
11use crate::renderer;
12use crate::runtime::UserInterface;
13use crate::runtime::user_interface;
14use crate::selector::Bounded;
15use crate::{Error, Selector};
16
17use std::borrow::Cow;
18use std::env;
19use std::fs;
20use std::io;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24pub struct Simulator<'a, Message, Theme = core::Theme, Renderer = renderer::Renderer> {
26 raw: UserInterface<'a, Message, Theme, Renderer>,
27 renderer: Renderer,
28 size: Size,
29 cursor: mouse::Cursor,
30 messages: Vec<Message>,
31}
32
33impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer>
34where
35 Theme: theme::Base,
36 Renderer: core::Renderer + core::renderer::Headless,
37{
38 pub fn new(element: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
40 Self::with_settings(Settings::default(), element)
41 }
42
43 pub fn with_settings(
45 settings: Settings,
46 element: impl Into<Element<'a, Message, Theme, Renderer>>,
47 ) -> Self {
48 Self::with_size(settings, window::Settings::default().size, element)
49 }
50
51 pub fn with_size(
53 settings: Settings,
54 size: impl Into<Size>,
55 element: impl Into<Element<'a, Message, Theme, Renderer>>,
56 ) -> Self {
57 let size = size.into();
58
59 for font in settings.fonts {
60 load_font(font).expect("Font must be valid");
61 }
62
63 let mut renderer = {
64 let backend = env::var("ICED_TEST_BACKEND").ok();
65
66 crate::futures::futures::executor::block_on(Renderer::new(
67 core::renderer::Settings {
68 default_font: settings.default_font,
69 default_text_size: settings.default_text_size,
70 },
71 backend.as_deref(),
72 ))
73 .expect("Create new headless renderer")
74 };
75
76 let raw = UserInterface::build(
77 element,
78 size,
79 user_interface::Cache::default(),
80 &mut renderer,
81 );
82
83 Simulator {
84 raw,
85 renderer,
86 size,
87 cursor: mouse::Cursor::Unavailable,
88 messages: Vec::new(),
89 }
90 }
91
92 pub fn find<S>(&mut self, selector: S) -> Result<S::Output, Error>
94 where
95 S: Selector + Send,
96 S::Output: Clone + Send,
97 {
98 use widget::Operation;
99
100 let description = selector.description();
101 let mut operation = selector.find();
102
103 self.raw.operate(
104 &self.renderer,
105 &mut widget::operation::black_box(&mut operation),
106 );
107
108 match operation.finish() {
109 widget::operation::Outcome::Some(output) => output.ok_or(Error::SelectorNotFound {
110 selector: description,
111 }),
112 _ => Err(Error::SelectorNotFound {
113 selector: description,
114 }),
115 }
116 }
117
118 pub fn point_at(&mut self, position: impl Into<Point>) {
122 self.cursor = mouse::Cursor::Available(position.into());
123 }
124
125 pub fn click<S>(&mut self, selector: S) -> Result<S::Output, Error>
131 where
132 S: Selector + Send,
133 S::Output: Bounded + Clone + Send + Sync + 'static,
134 {
135 let target = self.find(selector)?;
136
137 let Some(visible_bounds) = target.visible_bounds() else {
138 return Err(Error::TargetNotVisible {
139 target: Arc::new(target),
140 });
141 };
142
143 self.point_at(visible_bounds.center());
144
145 let _ = self.simulate(click());
146
147 Ok(target)
148 }
149
150 pub fn tap_key(&mut self, key: impl Into<keyboard::Key>) -> event::Status {
152 self.simulate(tap_key(key, None))
153 .first()
154 .copied()
155 .unwrap_or(event::Status::Ignored)
156 }
157
158 pub fn typewrite(&mut self, text: &str) -> event::Status {
160 let statuses = self.simulate(typewrite(text));
161
162 statuses
163 .into_iter()
164 .fold(event::Status::Ignored, event::Status::merge)
165 }
166
167 pub fn simulate(&mut self, events: impl IntoIterator<Item = Event>) -> Vec<event::Status> {
169 let events: Vec<Event> = events.into_iter().collect();
170
171 let (_state, statuses) =
172 self.raw
173 .update(&events, self.cursor, &mut self.renderer, &mut self.messages);
174
175 statuses
176 }
177
178 pub fn snapshot(&mut self, theme: &Theme) -> Result<Snapshot, Error> {
180 let base = theme.base();
181
182 let _ = self.raw.update(
183 &[Event::Window(window::Event::RedrawRequested(
184 time::Instant::now(),
185 ))],
186 self.cursor,
187 &mut self.renderer,
188 &mut self.messages,
189 );
190
191 self.raw.draw(
192 &mut self.renderer,
193 theme,
194 &core::renderer::Style {
195 text_color: base.text_color,
196 },
197 self.cursor,
198 );
199
200 let scale_factor = 2.0;
201
202 let physical_size = Size::new(
203 (self.size.width * scale_factor).round() as u32,
204 (self.size.height * scale_factor).round() as u32,
205 );
206
207 let rgba = self
208 .renderer
209 .screenshot(physical_size, scale_factor, base.background_color);
210
211 Ok(Snapshot {
212 screenshot: window::Screenshot::new(rgba, physical_size, scale_factor),
213 renderer: self.renderer.name(),
214 })
215 }
216
217 pub fn into_messages(self) -> impl Iterator<Item = Message> + use<Message, Theme, Renderer> {
219 self.messages.into_iter()
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct Snapshot {
226 screenshot: window::Screenshot,
227 renderer: String,
228}
229
230impl Snapshot {
231 pub fn matches_image(&self, path: impl AsRef<Path>) -> Result<bool, Error> {
237 let path = self.path(path, "png");
238
239 if path.exists() {
240 let file = fs::File::open(&path)?;
241 let decoder = png::Decoder::new(io::BufReader::new(file));
242
243 let mut reader = decoder.read_info()?;
244 let n = reader
245 .output_buffer_size()
246 .expect("snapshot should fit in memory");
247 let mut bytes = vec![0; n];
248 let info = reader.next_frame(&mut bytes)?;
249
250 Ok(self.screenshot.rgba == bytes[..info.buffer_size()])
251 } else {
252 if let Some(directory) = path.parent() {
253 fs::create_dir_all(directory)?;
254 }
255
256 let file = fs::File::create(path)?;
257
258 let mut encoder = png::Encoder::new(
259 file,
260 self.screenshot.size.width,
261 self.screenshot.size.height,
262 );
263 encoder.set_color(png::ColorType::Rgba);
264
265 let mut writer = encoder.write_header()?;
266 writer.write_image_data(&self.screenshot.rgba)?;
267 writer.finish()?;
268
269 Ok(true)
270 }
271 }
272
273 pub fn matches_hash(&self, path: impl AsRef<Path>) -> Result<bool, Error> {
279 use sha2::{Digest, Sha256};
280
281 let path = self.path(path, "sha256");
282
283 let hash = {
284 let mut hasher = Sha256::new();
285 hasher.update(&self.screenshot.rgba);
286 format!("{:x}", hasher.finalize())
287 };
288
289 if path.exists() {
290 let saved_hash = fs::read_to_string(&path)?;
291
292 Ok(hash == saved_hash)
293 } else {
294 if let Some(directory) = path.parent() {
295 fs::create_dir_all(directory)?;
296 }
297
298 fs::write(path, hash)?;
299 Ok(true)
300 }
301 }
302
303 fn path(&self, path: impl AsRef<Path>, extension: &str) -> PathBuf {
304 let path = path.as_ref();
305
306 path.with_file_name(format!(
307 "{name}-{renderer}",
308 name = path
309 .file_stem()
310 .map(std::ffi::OsStr::to_string_lossy)
311 .unwrap_or_default(),
312 renderer = self.renderer
313 ))
314 .with_extension(extension)
315 }
316}
317
318pub fn simulator<'a, Message, Theme, Renderer>(
322 element: impl Into<Element<'a, Message, Theme, Renderer>>,
323) -> Simulator<'a, Message, Theme, Renderer>
324where
325 Theme: theme::Base,
326 Renderer: core::Renderer + core::renderer::Headless,
327{
328 Simulator::new(element)
329}
330
331pub fn click() -> impl Iterator<Item = Event> {
333 [
334 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
335 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
336 ]
337 .into_iter()
338}
339
340pub fn press_key(key: impl Into<keyboard::Key>, text: Option<SmolStr>) -> Event {
342 let key = key.into();
343
344 Event::Keyboard(keyboard::Event::KeyPressed {
345 key: key.clone(),
346 modified_key: key,
347 physical_key: keyboard::key::Physical::Unidentified(
348 keyboard::key::NativeCode::Unidentified,
349 ),
350 location: keyboard::Location::Standard,
351 modifiers: keyboard::Modifiers::default(),
352 repeat: false,
353 text,
354 })
355}
356
357pub fn release_key(key: impl Into<keyboard::Key>) -> Event {
359 let key = key.into();
360
361 Event::Keyboard(keyboard::Event::KeyReleased {
362 key: key.clone(),
363 modified_key: key,
364 physical_key: keyboard::key::Physical::Unidentified(
365 keyboard::key::NativeCode::Unidentified,
366 ),
367 location: keyboard::Location::Standard,
368 modifiers: keyboard::Modifiers::default(),
369 })
370}
371
372pub fn tap_key(
374 key: impl Into<keyboard::Key>,
375 text: Option<SmolStr>,
376) -> impl Iterator<Item = Event> {
377 let key = key.into();
378
379 [press_key(key.clone(), text), release_key(key)].into_iter()
380}
381
382pub fn typewrite(text: &str) -> impl Iterator<Item = Event> + '_ {
384 text.chars()
385 .map(|c| SmolStr::new_inline(&c.to_string()))
386 .flat_map(|c| tap_key(keyboard::Key::Character(c.clone()), Some(c)))
387}
388
389fn load_font(font: impl Into<Cow<'static, [u8]>>) -> Result<(), Error> {
390 renderer::graphics::text::font_system()
391 .write()
392 .expect("Write to font system")
393 .load_font(font.into());
394
395 Ok(())
396}