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