Skip to main content

toddy_core/
testing.rs

1//! Test factory helpers for widget extension authors.
2//!
3//! Provides [`TestEnv`] for setting up a render environment and
4//! [`node`] / [`node_with_props`] / [`node_with_children`] for
5//! constructing test tree nodes.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use toddy_core::testing::*;
11//! use toddy_core::prelude::*;
12//!
13//! let test = TestEnv::default();
14//! let env = test.env();
15//! let element = my_extension.render(&node, &env);
16//! ```
17
18use iced::Theme;
19use serde_json::{Value, json};
20
21use crate::extensions::{ExtensionCaches, ExtensionDispatcher, RenderCtx, WidgetEnv};
22use crate::image_registry::ImageRegistry;
23use crate::protocol::TreeNode;
24use crate::widgets::WidgetCaches;
25
26// ---------------------------------------------------------------------------
27// TreeNode constructors
28// ---------------------------------------------------------------------------
29
30/// Create a minimal [`TreeNode`] with empty props and no children.
31pub fn node(id: &str, type_name: &str) -> TreeNode {
32    TreeNode {
33        id: id.to_string(),
34        type_name: type_name.to_string(),
35        props: json!({}),
36        children: vec![],
37    }
38}
39
40/// Create a [`TreeNode`] with the given props and no children.
41pub fn node_with_props(id: &str, type_name: &str, props: Value) -> TreeNode {
42    TreeNode {
43        id: id.to_string(),
44        type_name: type_name.to_string(),
45        props,
46        children: vec![],
47    }
48}
49
50/// Create a [`TreeNode`] with children and empty props.
51pub fn node_with_children(id: &str, type_name: &str, children: Vec<TreeNode>) -> TreeNode {
52    TreeNode {
53        id: id.to_string(),
54        type_name: type_name.to_string(),
55        props: json!({}),
56        children,
57    }
58}
59
60// ---------------------------------------------------------------------------
61// TestEnv: owns all render dependencies
62// ---------------------------------------------------------------------------
63
64/// Owns all the dependencies needed to construct a [`WidgetEnv`] for
65/// testing extension `render()` methods.
66///
67/// All fields are public so tests can customize before calling [`env`](Self::env).
68///
69/// # Example
70///
71/// ```ignore
72/// let test = TestEnv::default();
73/// let env = test.env();
74/// let element = my_extension.render(&node, &env);
75/// ```
76///
77/// With customization:
78///
79/// ```ignore
80/// let test = TestEnv {
81///     theme: Theme::Light,
82///     ..TestEnv::default()
83/// };
84/// let env = test.env();
85/// ```
86pub struct TestEnv {
87    pub ext_caches: ExtensionCaches,
88    pub widget_caches: WidgetCaches,
89    pub images: ImageRegistry,
90    pub theme: Theme,
91    pub dispatcher: ExtensionDispatcher,
92    pub default_text_size: Option<f32>,
93    pub default_font: Option<iced::Font>,
94}
95
96impl Default for TestEnv {
97    fn default() -> Self {
98        Self {
99            ext_caches: ExtensionCaches::new(),
100            widget_caches: WidgetCaches::new(),
101            images: ImageRegistry::new(),
102            theme: Theme::Dark,
103            dispatcher: ExtensionDispatcher::new(vec![]),
104            default_text_size: None,
105            default_font: None,
106        }
107    }
108}
109
110impl TestEnv {
111    /// Build a [`RenderCtx`] from the owned test state.
112    pub fn render_ctx(&self) -> RenderCtx<'_> {
113        RenderCtx {
114            caches: &self.widget_caches,
115            images: &self.images,
116            theme: &self.theme,
117            extensions: &self.dispatcher,
118            default_text_size: self.default_text_size,
119            default_font: self.default_font,
120        }
121    }
122
123    /// Borrow a [`WidgetEnv`] using an externally-held [`RenderCtx`].
124    ///
125    /// Usage:
126    /// ```ignore
127    /// let test = TestEnv::default();
128    /// let ctx = test.render_ctx();
129    /// let env = test.env(&ctx);
130    /// let element = my_extension.render(&node, &env);
131    /// ```
132    pub fn env<'a>(&'a self, ctx: &RenderCtx<'a>) -> WidgetEnv<'a> {
133        WidgetEnv {
134            caches: &self.ext_caches,
135            ctx: *ctx,
136        }
137    }
138}
139
140// ---------------------------------------------------------------------------
141// Tests
142// ---------------------------------------------------------------------------
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::extensions::GenerationCounter;
148    use crate::prop_helpers::{prop_f32, prop_str};
149
150    // -- TreeNode constructors ------------------------------------------------
151
152    #[test]
153    fn node_has_empty_props_and_no_children() {
154        let n = node("btn-1", "button");
155        assert_eq!(n.id, "btn-1");
156        assert_eq!(n.type_name, "button");
157        assert!(n.children.is_empty());
158        assert_eq!(n.props, json!({}));
159    }
160
161    #[test]
162    fn node_with_props_stores_props() {
163        let n = node_with_props("txt-1", "text", json!({"content": "hello", "size": 14}));
164        assert_eq!(n.props["content"], "hello");
165        assert_eq!(n.props["size"], 14);
166    }
167
168    #[test]
169    fn node_with_children_stores_children() {
170        let children = vec![node("a", "text"), node("b", "button")];
171        let n = node_with_children("col-1", "column", children);
172        assert_eq!(n.children.len(), 2);
173        assert_eq!(n.children[0].id, "a");
174        assert_eq!(n.children[1].id, "b");
175    }
176
177    #[test]
178    fn node_props_work_with_prop_helpers() {
179        let n = node_with_props("s-1", "sparkline", json!({"label": "cpu", "max": 100.0}));
180        let props = n.props.as_object();
181        assert_eq!(prop_str(props, "label"), Some("cpu".to_string()));
182        assert!((prop_f32(props, "max").unwrap() - 100.0).abs() < 0.001);
183    }
184
185    // -- TestEnv --------------------------------------------------------------
186
187    #[test]
188    fn default_env_has_no_text_defaults() {
189        let test = TestEnv::default();
190        let ctx = test.render_ctx();
191        let env = test.env(&ctx);
192        assert!(env.default_text_size().is_none());
193        assert!(env.default_font().is_none());
194    }
195
196    #[test]
197    fn default_env_has_empty_state() {
198        let test = TestEnv::default();
199        let ctx = test.render_ctx();
200        let env = test.env(&ctx);
201        assert!(!env.caches.contains("test", "anything"));
202        assert!(ctx.extensions.is_empty());
203    }
204
205    #[test]
206    fn env_inherits_text_defaults() {
207        let test = TestEnv {
208            default_text_size: Some(18.0),
209            default_font: Some(iced::Font::MONOSPACE),
210            ..TestEnv::default()
211        };
212
213        let ctx = test.render_ctx();
214        let env = test.env(&ctx);
215        assert_eq!(env.default_text_size(), Some(18.0));
216        assert_eq!(env.default_font(), Some(iced::Font::MONOSPACE));
217    }
218
219    #[test]
220    fn env_theme_is_customizable() {
221        let test = TestEnv {
222            theme: Theme::Light,
223            ..TestEnv::default()
224        };
225        let ctx = test.render_ctx();
226        let _env = test.env(&ctx);
227    }
228
229    // -- GenerationCounter in ExtensionCaches ---------------------------------
230
231    #[test]
232    fn generation_counter_lifecycle() {
233        let mut counter = GenerationCounter::new();
234        assert_eq!(counter.get(), 0);
235        counter.bump();
236        counter.bump();
237        assert_eq!(counter.get(), 2);
238    }
239
240    #[test]
241    fn generation_counter_in_caches() {
242        let mut test = TestEnv::default();
243        test.ext_caches
244            .insert("spark", "spark-1:gen", GenerationCounter::new());
245
246        let counter = test
247            .ext_caches
248            .get_mut::<GenerationCounter>("spark", "spark-1:gen")
249            .unwrap();
250        counter.bump();
251
252        let counter = test
253            .ext_caches
254            .get::<GenerationCounter>("spark", "spark-1:gen")
255            .unwrap();
256        assert_eq!(counter.get(), 1);
257    }
258}