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