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