Skip to main content

hyperchad_actions/
arb.rs

1//! Arbitrary value generation for property-based testing
2//!
3//! This module provides [`proptest::arbitrary::Arbitrary`] implementations for action types,
4//! enabling property-based testing of action serialization, deserialization, and processing.
5//!
6//! # Usage
7//!
8//! Enable the `arb` feature to use this module:
9//!
10//! ```toml
11//! [dependencies]
12//! hyperchad_actions = { version = "...", features = ["arb"] }
13//! ```
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use proptest::prelude::*;
19//! use hyperchad_actions::Action;
20//!
21//! proptest! {
22//!     #[test]
23//!     fn prop_action_roundtrip(action: Action) {
24//!         let json = serde_json::to_string(&action).unwrap();
25//!         let deserialized: Action = serde_json::from_str(&json).unwrap();
26//!         prop_assert_eq!(action, deserialized);
27//!     }
28//! }
29//! ```
30
31use hyperchad_transformer_models::Visibility;
32use moosicbox_arb::xml::XmlString;
33use proptest::prelude::*;
34
35use crate::{
36    Action, ActionEffect, ActionTrigger, ActionType, ElementTarget, LogLevel, StyleAction,
37};
38
39impl Arbitrary for ActionTrigger {
40    type Parameters = ();
41    type Strategy = BoxedStrategy<Self>;
42
43    /// Generates an arbitrary `ActionTrigger` for property-based testing
44    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
45        prop_oneof![
46            Just(Self::Click),
47            Just(Self::ClickOutside),
48            Just(Self::Hover),
49            Just(Self::Change),
50            Just(Self::Immediate),
51            Just(Self::HttpBeforeRequest),
52            Just(Self::HttpAfterRequest),
53            Just(Self::HttpRequestSuccess),
54            Just(Self::HttpRequestError),
55            Just(Self::HttpRequestAbort),
56            Just(Self::HttpRequestTimeout),
57            any::<XmlString>().prop_map(|s| Self::Event(s.0)),
58        ]
59        .boxed()
60    }
61}
62
63impl Arbitrary for StyleAction {
64    type Parameters = ();
65    type Strategy = BoxedStrategy<Self>;
66
67    /// Generates an arbitrary `StyleAction` for property-based testing
68    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
69        prop_oneof![
70            any::<Visibility>().prop_map(Self::SetVisibility),
71            any::<bool>().prop_map(Self::SetDisplay),
72        ]
73        .boxed()
74    }
75}
76
77impl Arbitrary for LogLevel {
78    type Parameters = ();
79    type Strategy = BoxedStrategy<Self>;
80
81    /// Generates an arbitrary `LogLevel` for property-based testing
82    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
83        prop_oneof![
84            Just(Self::Error),
85            Just(Self::Warn),
86            Just(Self::Info),
87            Just(Self::Debug),
88            Just(Self::Trace),
89        ]
90        .boxed()
91    }
92}
93
94#[cfg(feature = "logic")]
95impl Arbitrary for crate::logic::CalcValue {
96    type Parameters = ();
97    type Strategy = BoxedStrategy<Self>;
98
99    /// Generates an arbitrary `CalcValue` for property-based testing
100    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
101        any::<ElementTarget>()
102            .prop_map(|target| Self::Visibility { target })
103            .boxed()
104    }
105}
106
107#[cfg(feature = "logic")]
108impl Arbitrary for crate::logic::Value {
109    type Parameters = ();
110    type Strategy = BoxedStrategy<Self>;
111
112    /// Generates an arbitrary `Value` for property-based testing
113    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
114        prop_oneof![
115            any::<crate::logic::CalcValue>().prop_map(Self::Calc),
116            any::<Visibility>().prop_map(Self::Visibility),
117        ]
118        .boxed()
119    }
120}
121
122#[cfg(feature = "logic")]
123impl Arbitrary for crate::logic::Condition {
124    type Parameters = ();
125    type Strategy = BoxedStrategy<Self>;
126
127    /// Generates an arbitrary `Condition` for property-based testing
128    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
129        (any::<crate::logic::Value>(), any::<crate::logic::Value>())
130            .prop_map(|(a, b)| Self::Eq(a, b))
131            .boxed()
132    }
133}
134
135/// Strategy for generating a simple (non-recursive) `ActionType`
136#[cfg(feature = "logic")]
137fn simple_action_type_strategy() -> BoxedStrategy<ActionType> {
138    prop_oneof![
139        (any::<ElementTarget>(), any::<StyleAction>())
140            .prop_map(|(target, action)| ActionType::Style { target, action }),
141        any::<XmlString>().prop_map(|s| ActionType::Navigate { url: s.0 }),
142        (any::<XmlString>(), any::<LogLevel>()).prop_map(|(s, level)| ActionType::Log {
143            message: s.0,
144            level
145        }),
146        any::<XmlString>().prop_map(|s| ActionType::Custom { action: s.0 }),
147    ]
148    .boxed()
149}
150
151/// Strategy for generating a simple (non-recursive) `ActionEffect`
152#[cfg(feature = "logic")]
153fn simple_action_effect_strategy() -> BoxedStrategy<ActionEffect> {
154    (
155        simple_action_type_strategy(),
156        any::<Option<u64>>(),
157        any::<Option<u64>>(),
158        any::<Option<bool>>(),
159    )
160        .prop_map(|(action, delay_off, throttle, unique)| ActionEffect {
161            action,
162            delay_off,
163            throttle,
164            unique,
165        })
166        .boxed()
167}
168
169#[cfg(feature = "logic")]
170impl Arbitrary for crate::logic::If {
171    type Parameters = ();
172    type Strategy = BoxedStrategy<Self>;
173
174    /// Generates an arbitrary `If` conditional for property-based testing
175    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
176        (
177            any::<crate::logic::Condition>(),
178            prop::collection::vec(simple_action_effect_strategy(), 0..3),
179            prop::collection::vec(simple_action_effect_strategy(), 0..3),
180        )
181            .prop_map(|(condition, actions, else_actions)| Self {
182                condition,
183                actions,
184                else_actions,
185            })
186            .boxed()
187    }
188}
189
190impl Arbitrary for ActionType {
191    type Parameters = ();
192    type Strategy = BoxedStrategy<Self>;
193
194    /// Generates an arbitrary `ActionType` for property-based testing
195    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
196        #[cfg(feature = "logic")]
197        {
198            prop_oneof![
199                (any::<ElementTarget>(), any::<StyleAction>())
200                    .prop_map(|(target, action)| Self::Style { target, action }),
201                any::<XmlString>().prop_map(|s| Self::Navigate { url: s.0 }),
202                (any::<XmlString>(), any::<LogLevel>()).prop_map(|(s, level)| Self::Log {
203                    message: s.0,
204                    level
205                }),
206                any::<XmlString>().prop_map(|s| Self::Custom { action: s.0 }),
207                any::<crate::logic::If>().prop_map(Self::Logic),
208            ]
209            .boxed()
210        }
211
212        #[cfg(not(feature = "logic"))]
213        {
214            prop_oneof![
215                (any::<ElementTarget>(), any::<StyleAction>())
216                    .prop_map(|(target, action)| Self::Style { target, action }),
217                any::<XmlString>().prop_map(|s| Self::Navigate { url: s.0 }),
218                (any::<XmlString>(), any::<LogLevel>()).prop_map(|(s, level)| Self::Log {
219                    message: s.0,
220                    level
221                }),
222                any::<XmlString>().prop_map(|s| Self::Custom { action: s.0 }),
223            ]
224            .boxed()
225        }
226    }
227}
228
229impl Arbitrary for Action {
230    type Parameters = ();
231    type Strategy = BoxedStrategy<Self>;
232
233    /// Generates an arbitrary `Action` for property-based testing.
234    /// Uses `prop_map` instead of `prop_flat_map` to preserve shrinking behavior.
235    ///
236    /// When the trigger is `ActionTrigger::Event(name)`, the action must be wrapped
237    /// in `ActionType::Event { name, action }` with matching name.
238    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
239        (
240            any::<ActionTrigger>(),
241            any::<ActionType>(),
242            any::<Option<u64>>(),
243            any::<Option<u64>>(),
244            any::<Option<bool>>(),
245        )
246            .prop_map(|(trigger, action_type, delay_off, throttle, unique)| {
247                let action = match &trigger {
248                    ActionTrigger::Event(name) => ActionType::Event {
249                        name: name.clone(),
250                        action: Box::new(action_type),
251                    },
252                    _ => action_type,
253                };
254                Self {
255                    trigger,
256                    effect: ActionEffect {
257                        action,
258                        delay_off,
259                        throttle,
260                        unique,
261                    },
262                }
263            })
264            .boxed()
265    }
266}
267
268impl Arbitrary for ActionEffect {
269    type Parameters = ();
270    type Strategy = BoxedStrategy<Self>;
271
272    /// Generates an arbitrary `ActionEffect` for property-based testing
273    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
274        (
275            any::<ActionType>(),
276            any::<Option<u64>>(),
277            any::<Option<u64>>(),
278            any::<Option<bool>>(),
279        )
280            .prop_map(|(action, delay_off, throttle, unique)| Self {
281                action,
282                delay_off,
283                throttle,
284                unique,
285            })
286            .boxed()
287    }
288}