Skip to main content

freya_testing/
lib.rs

1//! Testing utilities for Freya applications.
2//!
3//! Simulate your app execution in a headless environment.
4//!
5//! Use [launch_test] or [TestingRunner] to instantiate a headless testing runner.
6//!
7//! # Examples
8//!
9//! Basic usage:
10//!
11//! ```rust,no_run
12//! use freya::prelude::*;
13//! use freya_testing::TestingRunner;
14//!
15//! fn app() -> impl IntoElement {
16//!     let mut state = use_consume::<State<i32>>();
17//!     rect().on_mouse_up(move |_| *state.write() += 1)
18//! }
19//!
20//! fn main() {
21//!     let (mut test, state) = TestingRunner::new(
22//!         app,
23//!         (300., 300.).into(),
24//!         |runner| runner.provide_root_context(|| State::create(0)),
25//!         1.,
26//!     );
27//!     test.sync_and_update();
28//!     // Simulate a mouse click
29//!     test.click_cursor((15., 15.));
30//!     assert_eq!(*state.peek(), 1);
31//! }
32//! ```
33//!
34//! For a runnable example see `examples/testing_events.rs` in the repository.
35
36use std::{
37    borrow::Cow,
38    cell::RefCell,
39    collections::HashMap,
40    fs::File,
41    io::Write,
42    path::PathBuf,
43    rc::Rc,
44    time::{
45        Duration,
46        Instant,
47    },
48};
49
50use freya_clipboard::copypasta::{
51    ClipboardContext,
52    ClipboardProvider,
53};
54use freya_components::{
55    cache::AssetCacher,
56    integration::integration,
57};
58use freya_core::{
59    integration::*,
60    prelude::*,
61};
62use freya_engine::prelude::{
63    EncodedImageFormat,
64    FontCollection,
65    FontMgr,
66    SkData,
67    TypefaceFontProvider,
68    raster_n32_premul,
69};
70use ragnarok::{
71    CursorPoint,
72    EventsExecutorRunner,
73    EventsMeasurerRunner,
74    NodesState,
75};
76use torin::prelude::{
77    LayoutNode,
78    Size2D,
79};
80
81pub mod prelude {
82    pub use freya_core::{
83        events::platform::*,
84        prelude::*,
85    };
86
87    pub use crate::{
88        DocRunner,
89        TestingRunner,
90        launch_doc,
91        launch_test,
92    };
93}
94
95type DocRunnerHook = Box<dyn FnOnce(&mut TestingRunner)>;
96
97pub struct DocRunner {
98    app: AppComponent,
99    size: Size2D,
100    scale_factor: f64,
101    hook: Option<DocRunnerHook>,
102    image_path: PathBuf,
103}
104
105impl DocRunner {
106    pub fn render(self) {
107        let (mut test, _) = TestingRunner::new(self.app, self.size, |_| {}, self.scale_factor);
108        if let Some(hook) = self.hook {
109            (hook)(&mut test);
110        }
111        test.render_to_file(self.image_path);
112    }
113
114    pub fn with_hook(mut self, hook: impl FnOnce(&mut TestingRunner) + 'static) -> Self {
115        self.hook = Some(Box::new(hook));
116        self
117    }
118
119    pub fn with_image_path(mut self, image_path: PathBuf) -> Self {
120        self.image_path = image_path;
121        self
122    }
123
124    pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
125        self.scale_factor = scale_factor;
126        self
127    }
128
129    pub fn with_size(mut self, size: Size2D) -> Self {
130        self.size = size;
131        self
132    }
133}
134
135pub fn launch_doc(app: impl Into<AppComponent>, path: impl Into<PathBuf>) -> DocRunner {
136    DocRunner {
137        app: app.into(),
138        size: Size2D::new(250., 250.),
139        scale_factor: 1.0,
140        hook: None,
141        image_path: path.into(),
142    }
143}
144
145pub fn launch_test(app: impl Into<AppComponent>) -> TestingRunner {
146    TestingRunner::new(app, Size2D::new(500., 500.), |_| {}, 1.0).0
147}
148
149pub struct TestingRunner {
150    nodes_state: NodesState<NodeId>,
151    runner: Runner,
152    tree: Rc<RefCell<Tree>>,
153    size: Size2D,
154
155    accessibility: AccessibilityTree,
156
157    events_receiver: futures_channel::mpsc::UnboundedReceiver<EventsChunk>,
158    events_sender: futures_channel::mpsc::UnboundedSender<EventsChunk>,
159
160    font_manager: FontMgr,
161    font_collection: FontCollection,
162
163    platform: Platform,
164
165    animation_clock: AnimationClock,
166    ticker_sender: RenderingTickerSender,
167
168    default_fonts: Vec<Cow<'static, str>>,
169    scale_factor: f64,
170}
171
172impl TestingRunner {
173    pub fn new<T>(
174        app: impl Into<AppComponent>,
175        size: Size2D,
176        hook: impl FnOnce(&mut Runner) -> T,
177        scale_factor: f64,
178    ) -> (Self, T) {
179        let (events_sender, events_receiver) = futures_channel::mpsc::unbounded();
180        let app = app.into();
181        let mut runner = Runner::new(move || integration(app.clone()).into_element());
182
183        runner.provide_root_context(ScreenReader::new);
184
185        let (mut ticker_sender, ticker) = RenderingTicker::new();
186        ticker_sender.set_overflow(true);
187        runner.provide_root_context(|| ticker);
188
189        let animation_clock = runner.provide_root_context(AnimationClock::new);
190
191        runner.provide_root_context(AssetCacher::create);
192
193        let tree = Tree::default();
194        let tree = Rc::new(RefCell::new(tree));
195
196        let platform = runner.provide_root_context({
197            let tree = tree.clone();
198            || Platform {
199                focused_accessibility_id: State::create(ACCESSIBILITY_ROOT_ID),
200                focused_accessibility_node: State::create(accesskit::Node::new(
201                    accesskit::Role::Window,
202                )),
203                root_size: State::create(size),
204                scale_factor: State::create(scale_factor),
205                navigation_mode: State::create(NavigationMode::NotKeyboard),
206                preferred_theme: State::create(PreferredTheme::Light),
207                is_app_focused: State::create(true),
208                accent_color: State::create(AccentColor::default()),
209                sender: Rc::new(move |user_event| {
210                    match user_event {
211                        UserEvent::RequestRedraw => {
212                            // Nothing
213                        }
214                        UserEvent::FocusAccessibilityNode(strategy) => {
215                            tree.borrow_mut().accessibility_diff.request_focus(strategy);
216                        }
217                        UserEvent::SetCursorIcon(_) => {
218                            // Nothing
219                        }
220                        UserEvent::Erased(_) => {
221                            // Nothing
222                        }
223                    }
224                }),
225            }
226        });
227
228        runner.provide_root_context(|| {
229            let clipboard: Option<Box<dyn ClipboardProvider>> = ClipboardContext::new()
230                .ok()
231                .map(|c| Box::new(c) as Box<dyn ClipboardProvider>);
232
233            State::create(clipboard)
234        });
235
236        runner.provide_root_context(|| tree.borrow().accessibility_generator.clone());
237
238        let hook_result = hook(&mut runner);
239
240        let mut font_collection = FontCollection::new();
241        let def_mgr = FontMgr::default();
242        let provider = TypefaceFontProvider::new();
243        let font_manager: FontMgr = provider.into();
244        font_collection.set_default_font_manager(def_mgr, None);
245        font_collection.set_dynamic_font_manager(font_manager.clone());
246        font_collection.paragraph_cache_mut().turn_on(false);
247
248        runner.provide_root_context(|| font_collection.clone());
249
250        let nodes_state = NodesState::default();
251        let accessibility = AccessibilityTree::default();
252
253        let mut runner = Self {
254            runner,
255            tree,
256            size,
257
258            accessibility,
259            platform,
260
261            nodes_state,
262            events_receiver,
263            events_sender,
264
265            font_manager,
266            font_collection,
267
268            animation_clock,
269            ticker_sender,
270
271            default_fonts: default_fonts(),
272            scale_factor,
273        };
274
275        runner.sync_and_update();
276
277        (runner, hook_result)
278    }
279
280    pub fn set_fonts(&mut self, fonts: HashMap<&str, &[u8]>) {
281        let mut provider = TypefaceFontProvider::new();
282        for (font_name, font_data) in fonts {
283            let ft_type = self
284                .font_collection
285                .fallback_manager()
286                .unwrap()
287                .new_from_data(font_data, None)
288                .unwrap_or_else(|| panic!("Failed to load font {font_name}."));
289            provider.register_typeface(ft_type, Some(font_name));
290        }
291        let font_manager: FontMgr = provider.into();
292        self.font_manager = font_manager.clone();
293        self.font_collection.set_dynamic_font_manager(font_manager);
294    }
295
296    pub fn set_default_fonts(&mut self, fonts: &[Cow<'static, str>]) {
297        self.default_fonts.clear();
298        self.default_fonts.extend_from_slice(fonts);
299        self.tree.borrow_mut().layout.reset();
300        self.tree.borrow_mut().text_cache.reset();
301        self.tree.borrow_mut().measure_layout(
302            self.size,
303            &mut self.font_collection,
304            &self.font_manager,
305            &self.events_sender,
306            self.scale_factor,
307            &self.default_fonts,
308        );
309        self.tree.borrow_mut().accessibility_diff.clear();
310        self.accessibility.focused_id = ACCESSIBILITY_ROOT_ID;
311        self.accessibility.init(&mut self.tree.borrow_mut());
312        self.sync_and_update();
313    }
314
315    pub async fn handle_events(&mut self) {
316        self.runner.handle_events().await
317    }
318
319    pub fn handle_events_immediately(&mut self) {
320        self.runner.handle_events_immediately()
321    }
322
323    pub fn sync_and_update(&mut self) {
324        while let Ok(events_chunk) = self.events_receiver.try_recv() {
325            match events_chunk {
326                EventsChunk::Processed(processed_events) => {
327                    let events_executor_adapter = EventsExecutorAdapter {
328                        runner: &mut self.runner,
329                    };
330                    events_executor_adapter.run(&mut self.nodes_state, processed_events);
331                }
332                EventsChunk::Batch(events) => {
333                    for event in events {
334                        self.runner.handle_event(
335                            event.node_id,
336                            event.name,
337                            event.data,
338                            event.bubbles,
339                        );
340                    }
341                }
342            }
343        }
344
345        let mutations = self.runner.sync_and_update();
346        self.runner.run_in(|| {
347            self.tree.borrow_mut().apply_mutations(mutations);
348        });
349        self.tree.borrow_mut().measure_layout(
350            self.size,
351            &mut self.font_collection,
352            &self.font_manager,
353            &self.events_sender,
354            self.scale_factor,
355            &self.default_fonts,
356        );
357
358        let accessibility_update = self
359            .accessibility
360            .process_updates(&mut self.tree.borrow_mut(), &self.events_sender);
361
362        self.platform
363            .focused_accessibility_id
364            .set_if_modified(accessibility_update.focus);
365        let node_id = self.accessibility.focused_node_id().unwrap();
366        let tree = self.tree.borrow();
367        let layout_node = tree.layout.get(&node_id).unwrap();
368        self.platform
369            .focused_accessibility_node
370            .set_if_modified(AccessibilityTree::create_node(node_id, layout_node, &tree));
371    }
372
373    /// Poll async tasks and events every `step` time for a total time of `duration`.
374    /// This is useful for animations for instance.
375    pub fn poll(&mut self, step: Duration, duration: Duration) {
376        let started = Instant::now();
377        while started.elapsed() < duration {
378            self.handle_events_immediately();
379            self.sync_and_update();
380            std::thread::sleep(step);
381            self.ticker_sender.broadcast_blocking(()).unwrap();
382        }
383    }
384
385    /// Poll async tasks and events every `step`, N times.
386    /// This is useful for animations for instance.
387    pub fn poll_n(&mut self, step: Duration, times: u32) {
388        for _ in 0..times {
389            self.handle_events_immediately();
390            self.sync_and_update();
391            std::thread::sleep(step);
392            self.ticker_sender.broadcast_blocking(()).unwrap();
393        }
394    }
395
396    pub fn send_event(&mut self, platform_event: PlatformEvent) {
397        let mut events_measurer_adapter = EventsMeasurerAdapter {
398            tree: &mut self.tree.borrow_mut(),
399            scale_factor: self.scale_factor,
400        };
401        let processed_events = events_measurer_adapter.run(
402            &mut vec![platform_event],
403            &mut self.nodes_state,
404            self.accessibility.focused_node_id(),
405        );
406        self.events_sender
407            .unbounded_send(EventsChunk::Processed(processed_events))
408            .unwrap();
409    }
410
411    pub fn move_cursor(&mut self, cursor: impl Into<CursorPoint>) {
412        self.send_event(PlatformEvent::Mouse {
413            name: MouseEventName::MouseMove,
414            cursor: cursor.into(),
415            button: Some(MouseButton::Left),
416        })
417    }
418
419    pub fn write_text(&mut self, text: impl ToString) {
420        let text = text.to_string();
421        self.send_event(PlatformEvent::Keyboard {
422            name: KeyboardEventName::KeyDown,
423            key: Key::Character(text),
424            code: Code::Unidentified,
425            modifiers: Modifiers::default(),
426        });
427        self.sync_and_update();
428    }
429
430    pub fn press_key(&mut self, key: Key) {
431        self.send_event(PlatformEvent::Keyboard {
432            name: KeyboardEventName::KeyDown,
433            key,
434            code: Code::Unidentified,
435            modifiers: Modifiers::default(),
436        });
437        self.sync_and_update();
438    }
439
440    pub fn press_cursor(&mut self, cursor: impl Into<CursorPoint>) {
441        let cursor = cursor.into();
442        self.send_event(PlatformEvent::Mouse {
443            name: MouseEventName::MouseDown,
444            cursor,
445            button: Some(MouseButton::Left),
446        });
447        self.sync_and_update();
448    }
449
450    pub fn release_cursor(&mut self, cursor: impl Into<CursorPoint>) {
451        let cursor = cursor.into();
452        self.send_event(PlatformEvent::Mouse {
453            name: MouseEventName::MouseUp,
454            cursor,
455            button: Some(MouseButton::Left),
456        });
457        self.sync_and_update();
458    }
459
460    pub fn click_cursor(&mut self, cursor: impl Into<CursorPoint>) {
461        let cursor = cursor.into();
462        self.send_event(PlatformEvent::Mouse {
463            name: MouseEventName::MouseDown,
464            cursor,
465            button: Some(MouseButton::Left),
466        });
467        self.sync_and_update();
468        self.send_event(PlatformEvent::Mouse {
469            name: MouseEventName::MouseUp,
470            cursor,
471            button: Some(MouseButton::Left),
472        });
473        self.sync_and_update();
474    }
475
476    pub fn press_touch(&mut self, location: impl Into<CursorPoint>) {
477        self.send_event(PlatformEvent::Touch {
478            name: TouchEventName::TouchStart,
479            location: location.into(),
480            finger_id: 0,
481            phase: TouchPhase::Started,
482            force: None,
483        });
484        self.sync_and_update();
485    }
486
487    pub fn move_touch(&mut self, location: impl Into<CursorPoint>) {
488        self.send_event(PlatformEvent::Touch {
489            name: TouchEventName::TouchMove,
490            location: location.into(),
491            finger_id: 0,
492            phase: TouchPhase::Moved,
493            force: None,
494        });
495        self.sync_and_update();
496    }
497
498    pub fn release_touch(&mut self, location: impl Into<CursorPoint>) {
499        self.send_event(PlatformEvent::Touch {
500            name: TouchEventName::TouchEnd,
501            location: location.into(),
502            finger_id: 0,
503            phase: TouchPhase::Ended,
504            force: None,
505        });
506        self.sync_and_update();
507    }
508
509    pub fn scroll(&mut self, cursor: impl Into<CursorPoint>, scroll: impl Into<CursorPoint>) {
510        let cursor = cursor.into();
511        let scroll = scroll.into();
512        self.send_event(PlatformEvent::Wheel {
513            name: WheelEventName::Wheel,
514            scroll,
515            cursor,
516            source: WheelSource::Device,
517        });
518        self.sync_and_update();
519    }
520
521    pub fn animation_clock(&mut self) -> &mut AnimationClock {
522        &mut self.animation_clock
523    }
524
525    pub fn render(&mut self) -> SkData {
526        let mut surface = raster_n32_premul((self.size.width as i32, self.size.height as i32))
527            .expect("Failed to create the surface.");
528
529        let render_pipeline = RenderPipeline {
530            font_collection: &mut self.font_collection,
531            font_manager: &self.font_manager,
532            tree: &self.tree.borrow(),
533            canvas: surface.canvas(),
534            scale_factor: self.scale_factor,
535            background: Color::WHITE,
536        };
537        render_pipeline.render();
538
539        let image = surface.image_snapshot();
540        let mut context = surface.direct_context();
541        image
542            .encode(context.as_mut(), EncodedImageFormat::PNG, None)
543            .expect("Failed to encode the snapshot.")
544    }
545
546    pub fn render_to_file(&mut self, path: impl Into<PathBuf>) {
547        let path = path.into();
548
549        let image = self.render();
550
551        let mut snapshot_file = File::create(path).expect("Failed to create the snapshot file.");
552
553        snapshot_file
554            .write_all(&image)
555            .expect("Failed to save the snapshot file.");
556    }
557
558    pub fn find<T>(
559        &self,
560        matcher: impl Fn(TestingNode, &dyn ElementExt) -> Option<T>,
561    ) -> Option<T> {
562        let mut matched = None;
563        {
564            let tree = self.tree.borrow();
565            tree.traverse_depth(|id| {
566                if matched.is_some() {
567                    return;
568                }
569                let element = tree.elements.get(&id).unwrap();
570                let node = TestingNode {
571                    tree: self.tree.clone(),
572                    id,
573                };
574                matched = matcher(node, element.as_ref());
575            });
576        }
577
578        matched
579    }
580
581    pub fn find_many<T>(
582        &self,
583        matcher: impl Fn(TestingNode, &dyn ElementExt) -> Option<T>,
584    ) -> Vec<T> {
585        let mut matched = Vec::new();
586        {
587            let tree = self.tree.borrow();
588            tree.traverse_depth(|id| {
589                let element = tree.elements.get(&id).unwrap();
590                let node = TestingNode {
591                    tree: self.tree.clone(),
592                    id,
593                };
594                if let Some(result) = matcher(node, element.as_ref()) {
595                    matched.push(result);
596                }
597            });
598        }
599
600        matched
601    }
602}
603
604pub struct TestingNode {
605    tree: Rc<RefCell<Tree>>,
606    id: NodeId,
607}
608
609impl TestingNode {
610    pub fn layout(&self) -> LayoutNode {
611        self.tree.borrow().layout.get(&self.id).cloned().unwrap()
612    }
613
614    pub fn children(&self) -> Vec<Self> {
615        let children = self
616            .tree
617            .borrow()
618            .children
619            .get(&self.id)
620            .cloned()
621            .unwrap_or_default();
622
623        children
624            .into_iter()
625            .map(|child_id| Self {
626                id: child_id,
627                tree: self.tree.clone(),
628            })
629            .collect()
630    }
631
632    pub fn is_visible(&self) -> bool {
633        let layout = self.layout();
634        let effect_state = self
635            .tree
636            .borrow()
637            .effect_state
638            .get(&self.id)
639            .cloned()
640            .unwrap();
641
642        effect_state.is_visible(&self.tree.borrow().layout, &layout.area)
643    }
644
645    pub fn element(&self) -> Rc<dyn ElementExt> {
646        self.tree
647            .borrow()
648            .elements
649            .get(&self.id)
650            .cloned()
651            .expect("Element does not exist.")
652    }
653}