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