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::{Event, Point, Settings, Size, SmolStr, Widget};
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 Message: 'a,
38 Theme: theme::Base + 'a,
39 Renderer: core::Renderer + core::renderer::Headless + 'a,
40{
41 pub fn new(element: impl Widget<Message, Theme, Renderer> + 'a) -> Self {
43 Self::with_settings(Settings::default(), element)
44 }
45
46 pub fn with_settings(
48 settings: Settings,
49 element: impl Widget<Message, Theme, Renderer> + 'a,
50 ) -> Self {
51 Self::with_size(settings, window::Settings::default().size, element)
52 }
53
54 pub fn with_size(
56 settings: Settings,
57 size: impl Into<Size>,
58 element: impl Widget<Message, Theme, Renderer> + 'a,
59 ) -> Self {
60 let size = size.into();
61
62 for font in settings.fonts {
63 load_font(font).expect("Font must be valid");
64 }
65
66 let mut renderer = {
67 let backend = env::var("ICED_TEST_BACKEND").ok();
68
69 crate::futures::futures::executor::block_on(Renderer::new(
70 core::renderer::Settings {
71 font: settings.font,
72 text_size: settings.text_size,
73 line_height: settings.line_height,
74 metrics_hinting: settings.metrics_hinting,
75 },
76 backend.as_deref(),
77 ))
78 .expect("Create new headless renderer")
79 };
80
81 let raw = UserInterface::build(
82 element,
83 size,
84 user_interface::Cache::default(),
85 &mut renderer,
86 );
87
88 Simulator {
89 raw,
90 renderer,
91 size,
92 cursor: mouse::Cursor::Unavailable,
93 messages: shell::Bus::new(),
94 }
95 }
96
97 pub fn find<S>(&mut self, selector: S) -> Result<S::Output, Error>
99 where
100 S: Selector + Send,
101 S::Output: Clone + Send,
102 {
103 use widget::Operation;
104
105 let description = selector.description();
106 let mut operation = selector.find();
107
108 self.raw.operate(
109 &self.renderer,
110 &mut widget::operation::black_box(&mut operation),
111 );
112
113 match operation.finish() {
114 widget::operation::Outcome::Some(output) => output.ok_or(Error::SelectorNotFound {
115 selector: description,
116 }),
117 _ => Err(Error::SelectorNotFound {
118 selector: description,
119 }),
120 }
121 }
122
123 pub fn point_at(&mut self, position: impl Into<Point>) {
127 self.cursor = mouse::Cursor::Available(position.into());
128 }
129
130 pub fn operate(&mut self, operation: &mut dyn widget::Operation) {
132 self.raw.operate(&self.renderer, operation);
133 }
134
135 pub fn rebuild(mut self, element: impl Widget<Message, Theme, Renderer> + 'a) -> Self {
138 let cache = self.raw.into_cache();
139
140 Self {
141 raw: UserInterface::build(element, self.size, cache, &mut self.renderer),
142 renderer: self.renderer,
143 size: self.size,
144 cursor: self.cursor,
145 messages: self.messages,
146 }
147 }
148
149 pub fn resize(mut self, size: impl Into<Size>) -> Self {
152 let size = size.into();
153
154 self.raw = self.raw.relayout(size, &mut self.renderer);
155 self.size = size;
156
157 self
158 }
159
160 pub fn click<S>(&mut self, selector: S) -> Result<S::Output, Error>
166 where
167 S: Selector + Send,
168 S::Output: Bounded + Clone + Send + Sync + 'static,
169 {
170 let target = self.find(selector)?;
171
172 let Some(visible_bounds) = target.visible_bounds() else {
173 return Err(Error::TargetNotVisible {
174 target: Arc::new(target),
175 });
176 };
177
178 self.point_at(visible_bounds.center());
179
180 let _ = self.simulate(click());
181
182 Ok(target)
183 }
184
185 pub fn tap_key(&mut self, key: impl Into<keyboard::Key>) -> event::Status {
187 self.simulate(tap_key(key, None))
188 .first()
189 .copied()
190 .unwrap_or(event::Status::Ignored)
191 }
192
193 pub fn typewrite(&mut self, text: &str) -> event::Status {
195 let statuses = self.simulate(typewrite(text));
196
197 statuses
198 .into_iter()
199 .fold(event::Status::Ignored, event::Status::merge)
200 }
201
202 pub fn scroll(&mut self, delta: mouse::ScrollDelta) -> event::Status {
209 let statuses = self.simulate(scroll(delta));
210
211 statuses
212 .into_iter()
213 .fold(event::Status::Ignored, event::Status::merge)
214 }
215
216 pub fn simulate(&mut self, events: impl IntoIterator<Item = Event>) -> Vec<event::Status> {
218 let events: Vec<Event> = events.into_iter().collect();
219
220 let (_state, statuses) = self.raw.update(
221 &window::Headless,
222 &shell::Waker::noop(),
223 &events,
224 self.cursor,
225 &mut self.renderer,
226 &mut self.messages,
227 );
228
229 statuses
230 }
231
232 pub fn draw(&mut self, theme: &Theme) {
236 let base = theme.base();
237
238 let _ = self.raw.update(
239 &window::Headless,
240 &shell::Waker::noop(),
241 &[Event::Window(window::Event::RedrawRequested(
242 time::Instant::now(),
243 ))],
244 self.cursor,
245 &mut self.renderer,
246 &mut self.messages,
247 );
248
249 self.raw.draw(
250 &mut self.renderer,
251 theme,
252 &core::renderer::Style {
253 text_color: base.text_color,
254 },
255 self.cursor,
256 );
257 }
258
259 pub fn snapshot(&mut self, theme: &Theme) -> Result<Snapshot, Error> {
261 let base = theme.base();
262
263 self.draw(theme);
264
265 let scale_factor = 2.0;
266
267 let physical_size = Size::new(
268 (self.size.width * scale_factor).round() as u32,
269 (self.size.height * scale_factor).round() as u32,
270 );
271
272 let rgba = self
273 .renderer
274 .screenshot(physical_size, scale_factor, base.background_color);
275
276 Ok(Snapshot {
277 screenshot: window::Screenshot::new(rgba, physical_size, scale_factor),
278 renderer: self.renderer.name(),
279 })
280 }
281
282 pub fn into_messages(self) -> impl Iterator<Item = Message> + use<Message, Theme, Renderer> {
284 self.messages.into_iter()
285 }
286
287 pub fn drain(&mut self) -> impl Iterator<Item = Message> {
289 self.messages.drain().map(|(message, _)| message)
290 }
291}
292
293#[derive(Debug, Clone)]
295pub struct Snapshot {
296 screenshot: window::Screenshot,
297 renderer: String,
298}
299
300impl Snapshot {
301 pub fn matches_image(&self, path: impl AsRef<Path>) -> Result<bool, Error> {
307 let path = self.path(path, "png");
308
309 if path.exists() {
310 let file = fs::File::open(&path)?;
311 let decoder = png::Decoder::new(io::BufReader::new(file));
312
313 let mut reader = decoder.read_info()?;
314 let n = reader
315 .output_buffer_size()
316 .expect("snapshot should fit in memory");
317 let mut bytes = vec![0; n];
318 let info = reader.next_frame(&mut bytes)?;
319
320 Ok(self.screenshot.rgba == bytes[..info.buffer_size()])
321 } else {
322 if let Some(directory) = path.parent() {
323 fs::create_dir_all(directory)?;
324 }
325
326 let file = fs::File::create(path)?;
327
328 let mut encoder = png::Encoder::new(
329 file,
330 self.screenshot.size.width,
331 self.screenshot.size.height,
332 );
333 encoder.set_color(png::ColorType::Rgba);
334
335 let mut writer = encoder.write_header()?;
336 writer.write_image_data(&self.screenshot.rgba)?;
337 writer.finish()?;
338
339 Ok(true)
340 }
341 }
342
343 pub fn matches_hash(&self, path: impl AsRef<Path>) -> Result<bool, Error> {
349 use sha2::{Digest, Sha256};
350
351 let path = self.path(path, "sha256");
352
353 let hash = {
354 let mut hasher = Sha256::new();
355 hasher.update(&self.screenshot.rgba);
356 format!("{:x}", hasher.finalize())
357 };
358
359 if path.exists() {
360 let saved_hash = fs::read_to_string(&path)?;
361
362 Ok(hash == saved_hash)
363 } else {
364 if let Some(directory) = path.parent() {
365 fs::create_dir_all(directory)?;
366 }
367
368 fs::write(path, hash)?;
369 Ok(true)
370 }
371 }
372
373 fn path(&self, path: impl AsRef<Path>, extension: &str) -> PathBuf {
374 let path = path.as_ref();
375
376 path.with_file_name(format!(
377 "{name}-{renderer}",
378 name = path
379 .file_stem()
380 .map(std::ffi::OsStr::to_string_lossy)
381 .unwrap_or_default(),
382 renderer = self.renderer
383 ))
384 .with_extension(extension)
385 }
386}
387
388pub fn simulator<'a, Message, Theme, Renderer>(
392 element: impl Widget<Message, Theme, Renderer> + 'a,
393) -> Simulator<'a, Message, Theme, Renderer>
394where
395 Message: 'a,
396 Theme: theme::Base + 'a,
397 Renderer: core::Renderer + core::renderer::Headless + 'a,
398{
399 Simulator::new(element)
400}
401
402pub fn click() -> impl Iterator<Item = Event> {
404 [
405 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
406 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
407 ]
408 .into_iter()
409}
410
411pub fn scroll(delta: mouse::ScrollDelta) -> impl Iterator<Item = Event> {
415 std::iter::once(Event::Mouse(mouse::Event::WheelScrolled { delta }))
416}
417
418pub fn press_key(key: impl Into<keyboard::Key>, text: Option<SmolStr>) -> Event {
420 let key = key.into();
421
422 Event::Keyboard(keyboard::Event::KeyPressed {
423 key: key.clone(),
424 modified_key: key,
425 physical_key: keyboard::key::Physical::Unidentified(
426 keyboard::key::NativeCode::Unidentified,
427 ),
428 location: keyboard::Location::Standard,
429 modifiers: keyboard::Modifiers::default(),
430 repeat: false,
431 text,
432 })
433}
434
435pub fn release_key(key: impl Into<keyboard::Key>) -> Event {
437 let key = key.into();
438
439 Event::Keyboard(keyboard::Event::KeyReleased {
440 key: key.clone(),
441 modified_key: key,
442 physical_key: keyboard::key::Physical::Unidentified(
443 keyboard::key::NativeCode::Unidentified,
444 ),
445 location: keyboard::Location::Standard,
446 modifiers: keyboard::Modifiers::default(),
447 })
448}
449
450pub fn tap_key(
452 key: impl Into<keyboard::Key>,
453 text: Option<SmolStr>,
454) -> impl Iterator<Item = Event> {
455 let key = key.into();
456
457 [press_key(key.clone(), text), release_key(key)].into_iter()
458}
459
460pub fn typewrite(text: &str) -> impl Iterator<Item = Event> + '_ {
462 text.chars()
463 .map(|c| SmolStr::new_inline(&c.to_string()))
464 .flat_map(|c| tap_key(keyboard::Key::Character(c.clone()), Some(c)))
465}
466
467fn load_font(font: Cow<'static, [u8]>) -> Result<(), font::Error> {
468 renderer::graphics::text::font_system()
469 .write()
470 .expect("Write to font system")
471 .load_font(font);
472
473 Ok(())
474}