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