freya_testing/
launch.rs

1use std::{
2    future::Future,
3    path::PathBuf,
4};
5
6use accesskit::{
7    Node,
8    Role,
9};
10use dioxus_core::{
11    fc_to_builder,
12    Element,
13    IntoDynNode,
14    VirtualDom,
15};
16use dioxus_core_macro::{
17    component,
18    rsx,
19    Props,
20};
21#[cfg(debug_assertions)]
22use dioxus_signals::{
23    GlobalSignal,
24    Readable,
25};
26use freya_components::NativeContainer;
27use freya_core::{
28    accessibility::{
29        AccessibilityTree,
30        ACCESSIBILITY_ROOT_ID,
31    },
32    dom::{
33        FreyaDOM,
34        SafeDOM,
35    },
36    event_loop_messages::EventLoopMessage,
37    events::NodesState,
38    platform::CursorIcon,
39    platform_state::{
40        NativePlatformState,
41        NavigationMode,
42        PlatformInformation,
43        PreferredTheme,
44    },
45    types::EventsQueue,
46};
47use freya_elements as dioxus_elements;
48use freya_engine::prelude::*;
49use tokio::{
50    runtime::Runtime,
51    sync::{
52        broadcast,
53        mpsc::unbounded_channel,
54        watch,
55    },
56};
57use torin::prelude::Size2D;
58
59use crate::{
60    config::TestingConfig,
61    test_handler::TestingHandler,
62    test_utils::TestUtils,
63    SCALE_FACTOR,
64};
65
66/// Run a Component in a headless testing environment.
67///
68/// ```rust
69/// # use freya_testing::prelude::*;
70/// # use freya::prelude::*;
71/// # let rt = tokio::runtime::Builder::new_current_thread()
72/// # .enable_all()
73/// # .build()
74/// # .unwrap();
75/// # let _guard = rt.enter();
76/// fn app() -> Element {
77///     rsx!(
78///         rect {
79///             label {
80///                 "Hello, World!"
81///             }
82///         }
83///     )
84/// }
85///
86/// # rt.block_on(async move {
87/// let mut utils = launch_test(app);
88///
89/// let root = utils.root();
90/// let rect = root.get(0);
91/// let label = rect.get(0);
92/// let text = label.get(0);
93///
94/// assert_eq!(text.text(), Some("Hello, World!"));
95/// # });
96/// ```
97pub fn launch_test(root: AppComponent) -> TestingHandler<()> {
98    launch_test_with_config(root, TestingConfig::default())
99}
100
101/// Run a Component in a headless testing environment
102pub fn launch_test_with_config<T: 'static + Clone>(
103    root: AppComponent,
104    config: TestingConfig<T>,
105) -> TestingHandler<T> {
106    let vdom = with_accessibility(root);
107    let fdom = FreyaDOM::default();
108    let sdom = SafeDOM::new(fdom);
109
110    let (event_emitter, event_receiver) = unbounded_channel();
111    let (platform_event_emitter, platform_event_receiver) = unbounded_channel::<EventLoopMessage>();
112    let (platform_sender, platform_receiver) = watch::channel(NativePlatformState {
113        focused_accessibility_id: ACCESSIBILITY_ROOT_ID,
114        focused_accessibility_node: Node::new(Role::Window),
115        preferred_theme: PreferredTheme::default(),
116        navigation_mode: NavigationMode::default(),
117        information: PlatformInformation::new(config.size, false, false, false),
118        scale_factor: SCALE_FACTOR,
119    });
120    let mut font_collection = FontCollection::new();
121    let font_mgr = FontMgr::default();
122    font_collection.set_dynamic_font_manager(font_mgr.clone());
123    font_collection.set_default_font_manager(font_mgr.clone(), None);
124
125    let mut handler = TestingHandler {
126        vdom,
127        events_queue: EventsQueue::new(),
128        nodes_state: NodesState::default(),
129        font_collection,
130        font_mgr,
131        event_emitter,
132        event_receiver,
133        utils: TestUtils { sdom },
134        config,
135        platform_event_emitter,
136        platform_event_receiver,
137        accessibility_tree: AccessibilityTree::new(ACCESSIBILITY_ROOT_ID),
138        ticker_sender: broadcast::channel(5).0,
139        cursor_icon: CursorIcon::default(),
140        platform_sender,
141        platform_receiver,
142    };
143
144    handler.init_doms();
145    handler.resize(handler.config.size);
146
147    handler
148}
149
150fn with_accessibility(app: AppComponent) -> VirtualDom {
151    #[derive(Clone)]
152    struct RootProps {
153        app: AppComponent,
154    }
155
156    #[allow(non_snake_case)]
157    fn Root(props: RootProps) -> Element {
158        #[allow(non_snake_case)]
159        let App = props.app;
160
161        rsx!(NativeContainer {
162            App {}
163        })
164    }
165
166    VirtualDom::new_with_props(Root, RootProps { app })
167}
168
169type AppComponent = fn() -> Element;
170
171#[component]
172pub fn Preview(children: Element) -> Element {
173    rsx!(
174        rect {
175            main_align: "center",
176            cross_align: "center",
177            width: "fill",
178            height: "fill",
179            spacing: "8",
180            {children}
181        }
182    )
183}
184
185pub fn launch_doc(root: AppComponent, size: Size2D, path: impl Into<PathBuf>) {
186    let path: PathBuf = path.into();
187    launch_doc_with_utils(root, size, move |mut utils| async move {
188        utils.wait_for_update().await;
189        utils.save_snapshot(&path);
190    });
191}
192
193pub fn launch_doc_with_utils<F: Future<Output = ()>>(
194    root: AppComponent,
195    size: Size2D,
196    cb: impl FnOnce(TestingHandler<()>) -> F,
197) {
198    let mut utils = launch_test(root);
199    utils.resize(size);
200    let rt = Runtime::new().unwrap();
201    rt.block_on(async move {
202        cb(utils).await;
203    });
204}