freya_testing/
lib.rs

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