freya_testing/
test_handler.rs

1use std::{
2    fs::File,
3    io::Write,
4    path::PathBuf,
5    sync::Arc,
6    time::Duration,
7};
8
9use accesskit::NodeId as AccessibilityId;
10use dioxus_core::{
11    Event,
12    VirtualDom,
13};
14use freya_core::{
15    accessibility::AccessibilityTree,
16    dom::SafeDOM,
17    event_loop_messages::EventLoopMessage,
18    events::{
19        process_events,
20        EventName,
21        NodesState,
22        PlatformEvent,
23        PlatformEventData,
24    },
25    layout::process_layout,
26    render::{
27        Compositor,
28        RenderPipeline,
29    },
30    states::AccessibilityNodeState,
31    style::default_fonts,
32    types::{
33        EventEmitter,
34        EventReceiver,
35        EventsQueue,
36        NativePlatformReceiver,
37        NativePlatformSender,
38    },
39};
40use freya_engine::prelude::{
41    raster_n32_premul,
42    Color,
43    Data,
44    EncodedImageFormat,
45    FontCollection,
46    FontMgr,
47};
48use freya_native_core::{
49    dioxus::NodeImmutableDioxusExt,
50    prelude::NodeImmutable,
51};
52use tokio::{
53    sync::{
54        broadcast,
55        mpsc::{
56            UnboundedReceiver,
57            UnboundedSender,
58        },
59    },
60    time::{
61        interval,
62        timeout,
63    },
64};
65use torin::{
66    geometry::{
67        Area,
68        Size2D,
69    },
70    prelude::CursorPoint,
71};
72use winit::{
73    event::MouseButton,
74    window::CursorIcon,
75};
76
77use crate::{
78    config::TestingConfig,
79    test_node::TestNode,
80    test_utils::TestUtils,
81    SCALE_FACTOR,
82};
83
84/// Manages the lifecycle of your tests.
85pub struct TestingHandler<T: 'static + Clone> {
86    pub(crate) vdom: VirtualDom,
87    pub(crate) utils: TestUtils,
88    pub(crate) event_emitter: EventEmitter,
89    pub(crate) event_receiver: EventReceiver,
90    pub(crate) platform_event_emitter: UnboundedSender<EventLoopMessage>,
91    pub(crate) platform_event_receiver: UnboundedReceiver<EventLoopMessage>,
92    pub(crate) events_queue: EventsQueue,
93    pub(crate) nodes_state: NodesState,
94    pub(crate) platform_sender: NativePlatformSender,
95    pub(crate) platform_receiver: NativePlatformReceiver,
96    pub(crate) font_collection: FontCollection,
97    pub(crate) font_mgr: FontMgr,
98    pub(crate) accessibility_tree: AccessibilityTree,
99    pub(crate) config: TestingConfig<T>,
100    pub(crate) ticker_sender: broadcast::Sender<()>,
101    pub(crate) cursor_icon: CursorIcon,
102}
103
104impl<T: 'static + Clone> TestingHandler<T> {
105    /// Sync the RealDOM with the VirtualDOM.
106    pub(crate) fn init_doms(&mut self) {
107        if let Some(state) = self.config.state.take() {
108            self.vdom.insert_any_root_context(Box::new(state));
109        }
110        self.vdom
111            .insert_any_root_context(Box::new(self.platform_event_emitter.clone()));
112        self.vdom
113            .insert_any_root_context(Box::new(self.platform_receiver.clone()));
114        self.vdom
115            .insert_any_root_context(Box::new(Arc::new(self.ticker_sender.subscribe())));
116        self.vdom.insert_any_root_context(Box::new(
117            self.utils.sdom.get_mut().accessibility_generator().clone(),
118        ));
119
120        let sdom = self.utils.sdom();
121        let mut fdom = sdom.get_mut();
122        fdom.init_dom(&mut self.vdom, SCALE_FACTOR as f32);
123    }
124
125    /// Get a mutable reference to the current [`TestingConfig`].
126    pub fn config(&mut self) -> &mut TestingConfig<T> {
127        &mut self.config
128    }
129
130    /// Get the current [CursorIcon].
131    pub fn cursor_icon(&self) -> CursorIcon {
132        self.cursor_icon
133    }
134
135    /// Get the [SafeDOM].
136    pub fn sdom(&self) -> &SafeDOM {
137        self.utils.sdom()
138    }
139
140    /// Get the current [AccessibilityId].
141    pub fn focus_id(&self) -> AccessibilityId {
142        self.accessibility_tree.focused_id
143    }
144
145    /// Apply the latest changes of the virtual dom.
146    pub async fn wait_for_update(&mut self) -> (bool, bool) {
147        self.wait_for_work(self.config.size());
148
149        let mut ticker = if self.config.event_loop_ticker {
150            Some(interval(Duration::from_millis(16)))
151        } else {
152            None
153        };
154
155        // Handle platform and VDOM events
156        loop {
157            let platform_ev = self.platform_event_receiver.try_recv();
158            let vdom_events = self.event_receiver.try_recv();
159
160            if vdom_events.is_err() && platform_ev.is_err() {
161                break;
162            }
163
164            if let Ok(ev) = platform_ev {
165                match ev {
166                    EventLoopMessage::RequestRerender => {
167                        if let Some(ticker) = ticker.as_mut() {
168                            ticker.tick().await;
169                            self.ticker_sender.send(()).unwrap();
170                            timeout(self.config.vdom_timeout(), self.vdom.wait_for_work())
171                                .await
172                                .ok();
173                        }
174                    }
175                    EventLoopMessage::FocusAccessibilityNode(strategy) => {
176                        let fdom = self.utils.sdom.get();
177                        let rdom = fdom.rdom();
178                        self.accessibility_tree
179                            .focus_node_with_strategy(strategy, rdom);
180                    }
181                    EventLoopMessage::SetCursorIcon(icon) => {
182                        self.cursor_icon = icon;
183                    }
184                    EventLoopMessage::RemeasureTextGroup(text_measurement) => {
185                        let fdom = self.utils.sdom.get();
186                        fdom.measure_paragraphs(text_measurement, SCALE_FACTOR);
187                    }
188                    _ => {}
189                }
190            }
191
192            if let Ok(events) = vdom_events {
193                let fdom = self.utils.sdom().get();
194                let rdom = fdom.rdom();
195                for event in events {
196                    if let Some(element_id) =
197                        rdom.get(event.node_id).and_then(|node| node.mounted_id())
198                    {
199                        let name = event.name.into();
200                        let data = event.data.any();
201                        let event = Event::new(data, event.bubbles);
202                        self.vdom.runtime().handle_event(name, event, element_id);
203                        self.vdom.process_events();
204                    }
205                }
206            }
207        }
208
209        timeout(self.config.vdom_timeout(), self.vdom.wait_for_work())
210            .await
211            .ok();
212
213        let (must_repaint, must_relayout) = self
214            .utils
215            .sdom()
216            .get_mut()
217            .render_mutations(&mut self.vdom, SCALE_FACTOR as f32);
218
219        self.wait_for_work(self.config.size());
220
221        self.ticker_sender.send(()).unwrap();
222
223        (must_repaint, must_relayout)
224    }
225
226    /// Wait for layout and events to be processed
227    fn wait_for_work(&mut self, size: Size2D) {
228        process_layout(
229            &self.utils.sdom().get(),
230            Area {
231                origin: (0.0, 0.0).into(),
232                size,
233            },
234            &mut self.font_collection,
235            SCALE_FACTOR as f32,
236            &default_fonts(),
237        );
238
239        let fdom = &self.utils.sdom().get_mut();
240        {
241            let rdom = fdom.rdom();
242            let layout = fdom.layout();
243            let mut dirty_accessibility_tree = fdom.accessibility_dirty_nodes();
244            let (tree, node_id) = self.accessibility_tree.process_updates(
245                rdom,
246                &layout,
247                &mut dirty_accessibility_tree,
248            );
249
250            // Notify the components
251            self.platform_sender.send_modify(|state| {
252                state.focused_accessibility_id = tree.focus;
253                let node_ref = rdom.get(node_id).unwrap();
254                let node_accessibility = node_ref.get::<AccessibilityNodeState>().unwrap();
255                let layout_node = layout.get(node_id).unwrap();
256                state.focused_accessibility_node =
257                    AccessibilityTree::create_node(&node_ref, layout_node, &node_accessibility)
258            });
259        }
260
261        process_events(
262            fdom,
263            &mut self.events_queue,
264            &self.event_emitter,
265            &mut self.nodes_state,
266            SCALE_FACTOR,
267            self.accessibility_tree.focused_node_id(),
268        );
269    }
270
271    /// Push an event to the events queue
272    ///
273    /// ```rust, no_run
274    /// # use freya_testing::prelude::*;
275    /// # use freya::prelude::*;
276    /// # let mut utils = launch_test(|| rsx!( rect { } ));
277    /// utils.push_event(TestEvent::Mouse {
278    ///     name: EventName::MouseDown,
279    ///     cursor: (490., 20.).into(),
280    ///     button: Some(MouseButton::Left),
281    /// });
282    /// ```
283    ///
284    /// For mouse **movements** and **clicks** you can use shortcuts like [TestingHandler::move_cursor] and [TestingHandler::click_cursor].
285    pub fn push_event(&mut self, event: impl Into<PlatformEvent>) {
286        self.events_queue.push(event.into());
287    }
288
289    /// Get the Root node.
290    pub fn root(&self) -> TestNode {
291        let root_id = {
292            let sdom = self.utils.sdom();
293            let fdom = sdom.get();
294            let rdom = fdom.rdom();
295            rdom.root_id()
296        };
297
298        self.utils
299            .get_node_by_id(root_id)
300            // Get get the first element because of `KeyboardNavigator`
301            .get(0)
302    }
303
304    /// Resize the simulated canvas.
305    ///
306    /// ```rust, no_run
307    /// # use freya_testing::prelude::*;
308    /// # use freya::prelude::*;
309    /// # let mut utils = launch_test(|| rsx!( rect { } ));
310    /// utils.resize((500., 250.).into());
311    /// ```
312    pub fn resize(&mut self, size: Size2D) {
313        self.config.size = size;
314        self.platform_sender.send_modify(|state| {
315            state.information.viewport_size = size;
316        });
317        self.utils.sdom().get_mut().layout().reset();
318        self.utils
319            .sdom()
320            .get_mut()
321            .compositor_dirty_area()
322            .unite_or_insert(&Area::new((0.0, 0.0).into(), size));
323    }
324
325    /// Render the app into a canvas and create a snapshot of it.
326    ///
327    /// ```rust, no_run
328    /// # use freya_testing::prelude::*;
329    /// # use freya::prelude::*;
330    /// # let mut utils = launch_test(|| rsx!( rect { } ));
331    /// utils.save_snapshot("./snapshot.png");
332    /// ```
333    pub fn create_snapshot(&mut self) -> Data {
334        let fdom = self.utils.sdom.get();
335        let (width, height) = self.config.size.to_i32().to_tuple();
336
337        // Create the main surface
338        let mut surface =
339            raster_n32_premul((width, height)).expect("Failed to create the surface.");
340        surface.canvas().clear(Color::WHITE);
341
342        // Create the dirty surface
343        let mut dirty_surface = surface
344            .new_surface_with_dimensions((width, height))
345            .expect("Failed to create the dirty surface.");
346        dirty_surface.canvas().clear(Color::WHITE);
347
348        let mut compositor = Compositor::default();
349
350        // Render to the canvas
351        let mut render_pipeline = RenderPipeline {
352            canvas_area: Area::from_size((width as f32, height as f32).into()),
353            rdom: fdom.rdom(),
354            compositor_dirty_area: &mut fdom.compositor_dirty_area(),
355            compositor_dirty_nodes: &mut fdom.compositor_dirty_nodes(),
356            compositor_cache: &mut fdom.compositor_cache(),
357            layers: &mut fdom.layers(),
358            layout: &mut fdom.layout(),
359            background: Color::WHITE,
360            surface: &mut surface,
361            dirty_surface: &mut dirty_surface,
362            compositor: &mut compositor,
363            scale_factor: SCALE_FACTOR as f32,
364            selected_node: None,
365            font_collection: &mut self.font_collection,
366            font_manager: &self.font_mgr,
367            default_fonts: &["Fira Sans".to_string()],
368            images_cache: &mut fdom.images_cache(),
369        };
370        render_pipeline.run();
371
372        // Capture snapshot
373        let image = surface.image_snapshot();
374        let mut context = surface.direct_context();
375        image
376            .encode(context.as_mut(), EncodedImageFormat::PNG, None)
377            .expect("Failed to encode the snapshot.")
378    }
379
380    /// Render the app into a canvas and save it into a file.
381    ///
382    /// ```rust, no_run
383    /// # use freya_testing::prelude::*;
384    /// # use freya::prelude::*;
385    /// # let mut utils = launch_test(|| rsx!( rect { } ));
386    /// utils.save_snapshot("./snapshot.png");
387    /// ```
388    pub fn save_snapshot(&mut self, snapshot_path: impl Into<PathBuf>) {
389        let mut snapshot_file =
390            File::create(snapshot_path.into()).expect("Failed to create the snapshot file.");
391        let snapshot_data = self.create_snapshot();
392
393        snapshot_file
394            .write_all(&snapshot_data)
395            .expect("Failed to save the snapshot file.");
396    }
397
398    /// Shorthand to simulate a cursor move to the given location.
399    ///
400    /// ```rust
401    /// # use freya_testing::prelude::*;
402    /// # use freya::prelude::*;
403    /// # let mut utils = launch_test(|| rsx!( rect { } ));
404    /// utils.move_cursor((5., 5.));
405    /// ```
406    pub async fn move_cursor(&mut self, cursor: impl Into<CursorPoint>) {
407        self.push_event(PlatformEvent {
408            name: EventName::MouseMove,
409            data: PlatformEventData::Mouse {
410                cursor: cursor.into(),
411                button: Some(MouseButton::Left),
412            },
413        });
414        self.wait_for_update().await;
415    }
416
417    /// Shorthand to simulate a click with cursor in the given location.
418    ///
419    /// ```rust
420    /// # use freya_testing::prelude::*;
421    /// # use freya::prelude::*;
422    /// # let mut utils = launch_test(|| rsx!( rect { } ));
423    /// utils.click_cursor((5., 5.));
424    /// ```
425    pub async fn click_cursor(&mut self, cursor: impl Into<CursorPoint> + Clone) {
426        self.push_event(PlatformEvent {
427            name: EventName::MouseDown,
428            data: PlatformEventData::Mouse {
429                cursor: cursor.clone().into(),
430                button: Some(MouseButton::Left),
431            },
432        });
433        self.wait_for_update().await;
434        self.push_event(PlatformEvent {
435            name: EventName::MouseUp,
436            data: PlatformEventData::Mouse {
437                cursor: cursor.into(),
438                button: Some(MouseButton::Left),
439            },
440        });
441        self.wait_for_update().await;
442    }
443}