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
97type PendingFonts = Rc<RefCell<Vec<(Cow<'static, str>, Bytes)>>>;
98
99pub struct DocRunner {
100    app: AppComponent,
101    size: Size2D,
102    scale_factor: f64,
103    hook: Option<DocRunnerHook>,
104    image_path: PathBuf,
105}
106
107impl DocRunner {
108    pub fn render(self) {
109        let (mut test, _) = TestingRunner::new(self.app, self.size, |_| {}, self.scale_factor);
110        if let Some(hook) = self.hook {
111            (hook)(&mut test);
112        }
113        test.render_to_file(self.image_path);
114    }
115
116    pub fn with_hook(mut self, hook: impl FnOnce(&mut TestingRunner) + 'static) -> Self {
117        self.hook = Some(Box::new(hook));
118        self
119    }
120
121    pub fn with_image_path(mut self, image_path: PathBuf) -> Self {
122        self.image_path = image_path;
123        self
124    }
125
126    pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
127        self.scale_factor = scale_factor;
128        self
129    }
130
131    pub fn with_size(mut self, size: Size2D) -> Self {
132        self.size = size;
133        self
134    }
135}
136
137pub fn launch_doc(app: impl Into<AppComponent>, path: impl Into<PathBuf>) -> DocRunner {
138    DocRunner {
139        app: app.into(),
140        size: Size2D::new(250., 250.),
141        scale_factor: 1.0,
142        hook: None,
143        image_path: path.into(),
144    }
145}
146
147pub fn launch_test(app: impl Into<AppComponent>) -> TestingRunner {
148    TestingRunner::new(app, Size2D::new(500., 500.), |_| {}, 1.0).0
149}
150
151pub struct TestingRunner {
152    nodes_state: NodesState<NodeId>,
153    runner: Runner,
154    tree: Rc<RefCell<Tree>>,
155    size: Size2D,
156
157    accessibility: AccessibilityTree,
158
159    events_receiver: futures_channel::mpsc::UnboundedReceiver<EventsChunk>,
160    events_sender: futures_channel::mpsc::UnboundedSender<EventsChunk>,
161
162    requested_focus_strategy: Rc<RefCell<Option<AccessibilityFocusStrategy>>>,
163    pending_fonts: PendingFonts,
164
165    font_provider: TypefaceFontProvider,
166    font_manager: FontMgr,
167    font_collection: FontCollection,
168
169    platform: Platform,
170
171    animation_clock: AnimationClock,
172    ticker_sender: RenderingTickerSender,
173
174    default_fonts: Vec<Cow<'static, str>>,
175    scale_factor: f64,
176}
177
178impl TestingRunner {
179    pub fn new<T>(
180        app: impl Into<AppComponent>,
181        size: Size2D,
182        hook: impl FnOnce(&mut Runner) -> T,
183        scale_factor: f64,
184    ) -> (Self, T) {
185        let (events_sender, events_receiver) = futures_channel::mpsc::unbounded();
186        let app = app.into();
187        let mut runner = Runner::new(move || integration(app.clone()).into_element());
188
189        runner.provide_root_context(GlobalContexts::default);
190
191        runner.provide_root_context(ScreenReader::new);
192
193        let (ticker_sender, ticker) = RenderingTicker::new();
194        runner.provide_root_context(|| ticker);
195
196        let animation_clock = runner.provide_root_context(AnimationClock::new);
197
198        runner.provide_root_context(AssetCacher::create);
199
200        let tree = Tree::default();
201        let tree = Rc::new(RefCell::new(tree));
202
203        let requested_focus_strategy: Rc<RefCell<Option<AccessibilityFocusStrategy>>> =
204            Rc::new(RefCell::new(None));
205
206        let mut font_collection = FontCollection::new();
207        let def_mgr = FontMgr::default();
208        let provider = TypefaceFontProvider::new();
209        let font_manager: FontMgr = provider.clone().into();
210        font_collection.set_default_font_manager(def_mgr, None);
211        font_collection.set_dynamic_font_manager(font_manager.clone());
212        font_collection.paragraph_cache_mut().turn_on(false);
213
214        let pending_fonts = Rc::new(RefCell::new(Vec::new()));
215
216        let platform = runner.provide_root_context({
217            let requested_focus_strategy = requested_focus_strategy.clone();
218            let pending_fonts = pending_fonts.clone();
219            || Platform {
220                focused_accessibility_id: State::create(ACCESSIBILITY_ROOT_ID),
221                focused_accessibility_node: State::create(accesskit::Node::new(
222                    accesskit::Role::Window,
223                )),
224                root_size: State::create(size),
225                scale_factor: State::create(scale_factor),
226                custom_scale_factor: State::create(1.0),
227                navigation_mode: State::create(NavigationMode::NotKeyboard),
228                preferred_theme: State::create(PreferredTheme::Light),
229                is_app_focused: State::create(true),
230                accent_color: State::create(AccentColor::default()),
231                sender: Rc::new(move |user_event| {
232                    match user_event {
233                        UserEvent::FocusAccessibilityNode(strategy) => {
234                            requested_focus_strategy.borrow_mut().replace(strategy);
235                        }
236                        UserEvent::LoadFont {
237                            font_name,
238                            font_data,
239                        } => {
240                            pending_fonts.borrow_mut().push((font_name, font_data));
241                        }
242                        UserEvent::RequestRedraw
243                        | UserEvent::SetCustomScaleFactor(_)
244                        | UserEvent::Erased(_) => {
245                            // Nothing
246                        }
247                    }
248                }),
249            }
250        });
251
252        runner.provide_root_context(|| {
253            let clipboard: Option<Box<dyn ClipboardProvider>> = ClipboardContext::new()
254                .ok()
255                .map(|c| Box::new(c) as Box<dyn ClipboardProvider>);
256
257            State::create(clipboard)
258        });
259
260        runner.provide_root_context(|| tree.borrow().accessibility_generator.clone());
261
262        let hook_result = hook(&mut runner);
263
264        runner.provide_root_context(|| font_collection.clone());
265
266        let nodes_state = NodesState::default();
267        let accessibility = AccessibilityTree::default();
268
269        let mut runner = Self {
270            runner,
271            tree,
272            size,
273
274            accessibility,
275            platform,
276
277            nodes_state,
278            events_receiver,
279            events_sender,
280
281            requested_focus_strategy,
282            pending_fonts,
283
284            font_provider: provider,
285            font_manager,
286            font_collection,
287
288            animation_clock,
289            ticker_sender,
290
291            default_fonts: default_fonts(),
292            scale_factor,
293        };
294
295        runner.sync_and_update();
296
297        (runner, hook_result)
298    }
299
300    pub fn set_fonts(&mut self, fonts: HashMap<&str, &[u8]>) {
301        for (font_name, font_data) in fonts {
302            self.register_font(font_name, font_data);
303        }
304        self.invalidate_text_layout();
305    }
306
307    fn register_font(&mut self, font_name: &str, font_data: &[u8]) {
308        let typeface = self
309            .font_collection
310            .fallback_manager()
311            .unwrap()
312            .new_from_data(SkData::new_copy(font_data), None)
313            .unwrap_or_else(|| panic!("Failed to load font {font_name}."));
314        self.font_provider
315            .register_typeface(typeface, Some(font_name));
316    }
317
318    fn invalidate_text_layout(&mut self) {
319        self.font_collection.clear_caches();
320        let mut tree = self.tree.borrow_mut();
321        tree.layout.reset();
322        tree.text_cache.reset();
323    }
324
325    pub fn set_default_fonts(&mut self, fonts: &[Cow<'static, str>]) {
326        self.default_fonts.clear();
327        self.default_fonts.extend_from_slice(fonts);
328        self.invalidate_text_layout();
329        self.tree.borrow_mut().measure_layout(
330            self.size,
331            &mut self.font_collection,
332            &self.font_manager,
333            &self.events_sender,
334            &mut self.nodes_state,
335            self.scale_factor,
336            &self.default_fonts,
337        );
338        self.tree.borrow_mut().accessibility_diff.clear();
339        self.accessibility.focused_id = ACCESSIBILITY_ROOT_ID;
340        self.accessibility.init(&mut self.tree.borrow_mut(), "");
341        self.sync_and_update();
342    }
343
344    /// Run a closure inside the app runtime.
345    pub fn run_in<T>(&self, run: impl FnOnce() -> T) -> T {
346        self.runner.run_in(run)
347    }
348
349    pub async fn handle_events(&mut self) {
350        self.runner.handle_events().await
351    }
352
353    pub fn handle_events_immediately(&mut self) {
354        self.runner.handle_events_immediately()
355    }
356
357    pub fn sync_and_update(&mut self) {
358        if let Some(strategy) = self.requested_focus_strategy.borrow_mut().take() {
359            self.tree
360                .borrow_mut()
361                .accessibility_diff
362                .request_focus(strategy);
363        }
364
365        while let Ok(events_chunk) = self.events_receiver.try_recv() {
366            match events_chunk {
367                EventsChunk::Processed(processed_events) => {
368                    let events_executor_adapter = EventsExecutorAdapter {
369                        runner: &mut self.runner,
370                    };
371                    events_executor_adapter.run(&mut self.nodes_state, processed_events);
372                }
373                EventsChunk::Batch(events) => {
374                    for event in events {
375                        self.runner.handle_event(
376                            event.node_id,
377                            event.name,
378                            event.data,
379                            event.bubbles,
380                        );
381                    }
382                }
383            }
384        }
385
386        let mutations = self.runner.sync_and_update();
387        let result = self.runner.run_in(|| {
388            self.tree
389                .borrow_mut()
390                .apply_mutations(mutations, self.scale_factor as f32)
391        });
392        if let Some(strategy) = result.auto_focus {
393            self.requested_focus_strategy.borrow_mut().replace(strategy);
394        }
395        let pending_fonts = self.pending_fonts.take();
396        if !pending_fonts.is_empty() {
397            for (font_name, font_data) in pending_fonts {
398                self.register_font(&font_name, &font_data);
399            }
400            self.invalidate_text_layout();
401        }
402        self.tree.borrow_mut().measure_layout(
403            self.size,
404            &mut self.font_collection,
405            &self.font_manager,
406            &self.events_sender,
407            &mut self.nodes_state,
408            self.scale_factor,
409            &self.default_fonts,
410        );
411
412        let accessibility_update = self.accessibility.process_updates(
413            &mut self.tree.borrow_mut(),
414            &self.events_sender,
415            "",
416        );
417
418        self.platform
419            .focused_accessibility_id
420            .set_if_modified(accessibility_update.focus);
421        let node_id = self.accessibility.focused_node_id().unwrap();
422        let tree = self.tree.borrow();
423        let layout_node = tree.layout.get(&node_id).unwrap();
424        self.platform
425            .focused_accessibility_node
426            .set_if_modified(AccessibilityTree::create_node(
427                node_id,
428                layout_node,
429                &tree,
430                "",
431            ));
432    }
433
434    /// Poll async tasks and events every `step` time for a total time of `duration`.
435    /// This is useful for animations for instance.
436    pub fn poll(&mut self, step: Duration, duration: Duration) {
437        let started = Instant::now();
438        while started.elapsed() < duration {
439            self.handle_events_immediately();
440            self.sync_and_update();
441            std::thread::sleep(step);
442            self.ticker_sender.notify();
443        }
444    }
445
446    /// Poll async tasks and events every `step`, N times.
447    /// This is useful for animations for instance.
448    pub fn poll_n(&mut self, step: Duration, times: u32) {
449        for _ in 0..times {
450            self.handle_events_immediately();
451            self.sync_and_update();
452            std::thread::sleep(step);
453            self.ticker_sender.notify();
454        }
455    }
456
457    /// Resolve the [CursorIcon] for the currently hovered nodes.
458    pub fn cursor_icon(&self) -> CursorIcon {
459        self.tree.borrow().cursor_icon(&self.nodes_state)
460    }
461
462    pub fn send_event(&mut self, platform_event: PlatformEvent) {
463        let mut events_measurer_adapter = EventsMeasurerAdapter {
464            tree: &mut self.tree.borrow_mut(),
465            scale_factor: self.scale_factor,
466        };
467        let processed_events = events_measurer_adapter.run(
468            &mut vec![platform_event],
469            &mut self.nodes_state,
470            self.accessibility.focused_node_id(),
471        );
472        self.events_sender
473            .unbounded_send(EventsChunk::Processed(processed_events))
474            .unwrap();
475    }
476
477    pub fn move_cursor(&mut self, cursor: impl Into<CursorPoint>) {
478        self.send_event(PlatformEvent::Mouse {
479            name: MouseEventName::MouseMove,
480            cursor: cursor.into(),
481            button: Some(MouseButton::Left),
482        })
483    }
484
485    pub fn write_text(&mut self, text: impl ToString) {
486        let text = text.to_string();
487        self.send_event(PlatformEvent::Keyboard {
488            name: KeyboardEventName::KeyDown,
489            key: Key::Character(text),
490            code: Code::Unidentified,
491            modifiers: Modifiers::default(),
492        });
493        self.sync_and_update();
494    }
495
496    pub fn press_key(&mut self, key: Key) {
497        self.send_event(PlatformEvent::Keyboard {
498            name: KeyboardEventName::KeyDown,
499            key,
500            code: Code::Unidentified,
501            modifiers: Modifiers::default(),
502        });
503        self.sync_and_update();
504    }
505
506    pub fn press_cursor(&mut self, cursor: impl Into<CursorPoint>) {
507        let cursor = cursor.into();
508        self.send_event(PlatformEvent::Mouse {
509            name: MouseEventName::MouseDown,
510            cursor,
511            button: Some(MouseButton::Left),
512        });
513        self.sync_and_update();
514    }
515
516    pub fn release_cursor(&mut self, cursor: impl Into<CursorPoint>) {
517        let cursor = cursor.into();
518        self.send_event(PlatformEvent::Mouse {
519            name: MouseEventName::MouseUp,
520            cursor,
521            button: Some(MouseButton::Left),
522        });
523        self.sync_and_update();
524    }
525
526    pub fn click_cursor(&mut self, cursor: impl Into<CursorPoint>) {
527        let cursor = cursor.into();
528        self.send_event(PlatformEvent::Mouse {
529            name: MouseEventName::MouseDown,
530            cursor,
531            button: Some(MouseButton::Left),
532        });
533        self.sync_and_update();
534        self.send_event(PlatformEvent::Mouse {
535            name: MouseEventName::MouseUp,
536            cursor,
537            button: Some(MouseButton::Left),
538        });
539        self.sync_and_update();
540    }
541
542    pub fn press_touch(&mut self, location: impl Into<CursorPoint>) {
543        self.send_event(PlatformEvent::Touch {
544            name: TouchEventName::TouchStart,
545            location: location.into(),
546            finger_id: 0,
547            phase: TouchPhase::Started,
548            force: None,
549        });
550        self.sync_and_update();
551    }
552
553    pub fn move_touch(&mut self, location: impl Into<CursorPoint>) {
554        self.send_event(PlatformEvent::Touch {
555            name: TouchEventName::TouchMove,
556            location: location.into(),
557            finger_id: 0,
558            phase: TouchPhase::Moved,
559            force: None,
560        });
561        self.sync_and_update();
562    }
563
564    pub fn release_touch(&mut self, location: impl Into<CursorPoint>) {
565        self.send_event(PlatformEvent::Touch {
566            name: TouchEventName::TouchEnd,
567            location: location.into(),
568            finger_id: 0,
569            phase: TouchPhase::Ended,
570            force: None,
571        });
572        self.sync_and_update();
573    }
574
575    pub fn scroll(&mut self, cursor: impl Into<CursorPoint>, scroll: impl Into<CursorPoint>) {
576        let cursor = cursor.into();
577        let scroll = scroll.into();
578        self.send_event(PlatformEvent::Wheel {
579            name: WheelEventName::Wheel,
580            scroll,
581            cursor,
582            source: WheelSource::Device,
583        });
584        self.sync_and_update();
585        // Refresh hover states after the scroll
586        self.send_event(PlatformEvent::Mouse {
587            name: MouseEventName::MouseMove,
588            cursor,
589            button: None,
590        });
591        self.sync_and_update();
592    }
593
594    pub fn animation_clock(&mut self) -> &mut AnimationClock {
595        &mut self.animation_clock
596    }
597
598    pub fn render(&mut self) -> SkData {
599        let mut surface = raster_n32_premul((self.size.width as i32, self.size.height as i32))
600            .expect("Failed to create the surface.");
601
602        let render_pipeline = RenderPipeline {
603            font_collection: &mut self.font_collection,
604            font_manager: &self.font_manager,
605            tree: &self.tree.borrow(),
606            canvas: surface.canvas(),
607            scale_factor: self.scale_factor,
608            background: Color::WHITE,
609        };
610        render_pipeline.render();
611
612        let image = surface.image_snapshot();
613        let mut context = surface.direct_context();
614        image
615            .encode(context.as_mut(), EncodedImageFormat::PNG, None)
616            .expect("Failed to encode the snapshot.")
617    }
618
619    pub fn render_to_file(&mut self, path: impl Into<PathBuf>) {
620        let path = path.into();
621
622        let image = self.render();
623
624        let mut snapshot_file = File::create(path).expect("Failed to create the snapshot file.");
625
626        snapshot_file
627            .write_all(&image)
628            .expect("Failed to save the snapshot file.");
629    }
630
631    pub fn find<T>(
632        &self,
633        matcher: impl Fn(TestingNode, &dyn ElementExt) -> Option<T>,
634    ) -> Option<T> {
635        let mut matched = None;
636        {
637            let tree = self.tree.borrow();
638            tree.traverse_depth(|id| {
639                if matched.is_some() {
640                    return;
641                }
642                let element = tree.elements.get(&id).unwrap();
643                let node = TestingNode {
644                    tree: self.tree.clone(),
645                    id,
646                };
647                matched = matcher(node, element.as_ref());
648            });
649        }
650
651        matched
652    }
653
654    pub fn find_many<T>(
655        &self,
656        matcher: impl Fn(TestingNode, &dyn ElementExt) -> Option<T>,
657    ) -> Vec<T> {
658        let mut matched = Vec::new();
659        {
660            let tree = self.tree.borrow();
661            tree.traverse_depth(|id| {
662                let element = tree.elements.get(&id).unwrap();
663                let node = TestingNode {
664                    tree: self.tree.clone(),
665                    id,
666                };
667                if let Some(result) = matcher(node, element.as_ref()) {
668                    matched.push(result);
669                }
670            });
671        }
672
673        matched
674    }
675}
676
677pub struct TestingNode {
678    tree: Rc<RefCell<Tree>>,
679    id: NodeId,
680}
681
682impl TestingNode {
683    pub fn layout(&self) -> LayoutNode {
684        self.tree.borrow().layout.get(&self.id).cloned().unwrap()
685    }
686
687    pub fn children(&self) -> Vec<Self> {
688        let children = self
689            .tree
690            .borrow()
691            .children
692            .get(&self.id)
693            .cloned()
694            .unwrap_or_default();
695
696        children
697            .into_iter()
698            .map(|child_id| Self {
699                id: child_id,
700                tree: self.tree.clone(),
701            })
702            .collect()
703    }
704
705    pub fn is_visible(&self) -> bool {
706        let layout = self.layout();
707        let effect_state = self
708            .tree
709            .borrow()
710            .effect_state
711            .get(&self.id)
712            .cloned()
713            .unwrap();
714
715        effect_state.is_visible(&self.tree.borrow().layout, &layout.area)
716    }
717
718    pub fn element(&self) -> Rc<dyn ElementExt> {
719        self.tree
720            .borrow()
721            .elements
722            .get(&self.id)
723            .cloned()
724            .expect("Element does not exist.")
725    }
726}