Skip to main content

dioxus_field/
testing.rs

1//! Reusable assertions for widget registry conformance tests.
2//!
3//! Registry tests wire these probes into their real components, drive the component through its
4//! normal interaction path, then call the corresponding assertion. The probes deliberately do not
5//! prescribe a rendered element or DOM event because those details belong to each widget.
6//!
7//! # Conformance levels
8//!
9//! The convention has two levels, and the kit certifies each:
10//!
11//! - **Trio-conformant** (no dependency on this crate): the widget honors the `value` /
12//!   `on_change` / `on_commit` prop trio plus attribute spread. Applicable tests:
13//!   [`CommitOrderProbe`] and [`ChangeOriginProbe`] (trio-only widgets imply
14//!   [`ChangeOrigin::User`]).
15//! - **Field-aware**: the widget additionally resolves the Field Context. Applicable tests: the
16//!   three resolution-precedence assertions, [`FocusRoundTripProbe`], and
17//!   [`assert_field_part_ids`].
18//!
19//! # Example
20//!
21//! The [runnable interaction-probe adapter] demonstrates how callbacks created during rendering
22//! reach a registry-owned driver. The [complete conformance test] exercises all five required tests
23//! against a minimal field-aware widget.
24//!
25//! [runnable interaction-probe adapter]: https://docs.rs/crate/dioxus-field/latest/source/examples/conformance.rs
26//! [complete conformance test]: https://docs.rs/crate/dioxus-field/latest/source/tests/conformance.rs
27
28use std::{
29    cell::{Cell, RefCell},
30    fmt::Debug,
31    rc::Rc,
32};
33
34use dioxus_core::{Attribute, AttributeValue, Callback};
35use dioxus_signals::ReadSignal;
36
37use crate::{Binding, ChangeOrigin, FieldMeta, FieldMetaOverrides};
38
39/// Records the relative order of a widget commit and its containing submit handler.
40#[derive(Clone, Debug, Default)]
41pub struct CommitOrderProbe {
42    events: Rc<RefCell<Vec<CommitOrderEvent>>>,
43}
44
45impl CommitOrderProbe {
46    /// Creates an empty commit-order probe.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Returns the callback to wire to the widget's `on_commit` path.
52    pub fn on_commit(&self) -> Callback<()> {
53        let events = Rc::clone(&self.events);
54        Callback::new(move |()| events.borrow_mut().push(CommitOrderEvent::Commit))
55    }
56
57    /// Returns the callback to invoke from the containing submit handler.
58    pub fn on_submit(&self) -> Callback<()> {
59        let events = Rc::clone(&self.events);
60        Callback::new(move |()| events.borrow_mut().push(CommitOrderEvent::Submit))
61    }
62
63    /// Asserts that one commit was synchronously observed before one submit.
64    ///
65    /// # Panics
66    ///
67    /// Panics when either callback was omitted, repeated, or observed out of order.
68    pub fn assert_commit_before_submit(&self) {
69        assert_eq!(
70            *self.events.borrow(),
71            [CommitOrderEvent::Commit, CommitOrderEvent::Submit],
72            "the widget must synchronously commit exactly once before submit handling runs"
73        );
74    }
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78enum CommitOrderEvent {
79    Commit,
80    Submit,
81}
82
83/// Records values written through a [`Binding`] together with their [`ChangeOrigin`].
84#[derive(Debug)]
85pub struct ChangeOriginProbe<T> {
86    writes: Rc<RefCell<Vec<(T, ChangeOrigin)>>>,
87}
88
89impl<T> ChangeOriginProbe<T> {
90    /// Creates an empty write probe.
91    pub fn new() -> Self {
92        Self::default()
93    }
94}
95
96impl<T: 'static> ChangeOriginProbe<T> {
97    /// Creates a binding whose writes are recorded by this probe.
98    pub fn binding(&self, read: ReadSignal<T>) -> Binding<T> {
99        self.binding_with_commit(read, Callback::new(|()| {}))
100    }
101
102    /// Creates a binding whose writes are recorded and whose commits use `on_commit`.
103    pub fn binding_with_commit(&self, read: ReadSignal<T>, on_commit: Callback<()>) -> Binding<T> {
104        let writes = Rc::clone(&self.writes);
105
106        Binding::new(
107            read,
108            Callback::new(move |write| writes.borrow_mut().push(write)),
109            on_commit,
110        )
111    }
112}
113
114impl<T: Debug + PartialEq> ChangeOriginProbe<T> {
115    /// Asserts the complete ordered sequence of value and origin pairs.
116    ///
117    /// # Panics
118    ///
119    /// Panics when the observed writes differ from `expected`.
120    pub fn assert_writes(&self, expected: &[(T, ChangeOrigin)]) {
121        assert_eq!(
122            self.writes.borrow().as_slice(),
123            expected,
124            "widget writes must retain their change origin"
125        );
126    }
127}
128
129impl<T> Clone for ChangeOriginProbe<T> {
130    fn clone(&self) -> Self {
131        Self {
132            writes: Rc::clone(&self.writes),
133        }
134    }
135}
136
137impl<T> Default for ChangeOriginProbe<T> {
138    fn default() -> Self {
139        Self {
140            writes: Rc::new(RefCell::new(Vec::new())),
141        }
142    }
143}
144
145/// The two independently overridable metadata flags required by the convention.
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub struct OverridableMetaFlags {
148    /// The resolved invalid state.
149    pub invalid: bool,
150    /// The resolved disabled state.
151    pub disabled: bool,
152}
153
154impl OverridableMetaFlags {
155    /// Creates one observed or expected pair of metadata flags.
156    pub const fn new(invalid: bool, disabled: bool) -> Self {
157        Self { invalid, disabled }
158    }
159}
160
161/// Asserts explicit binding, context binding, then internal-state resolution precedence.
162///
163/// The registry adapter should expose the binding its widget resolved in the first two scenarios
164/// and the value observed after writing its uncontrolled binding in the final scenario.
165///
166/// # Panics
167///
168/// Panics when either resolved binding has the wrong identity or internal state did not retain its
169/// write.
170#[allow(
171    clippy::needless_pass_by_value,
172    reason = "owned observed values keep the assertion API convenient for registry tests"
173)]
174pub fn assert_binding_resolution_precedence<T: Debug + PartialEq + 'static>(
175    resolved_with_explicit: &Binding<T>,
176    explicit: &Binding<T>,
177    resolved_with_context: &Binding<T>,
178    context: &Binding<T>,
179    internal_value: T,
180    expected_internal_value: T,
181) {
182    assert!(
183        resolved_with_explicit == explicit,
184        "an explicit binding must win over Field Context"
185    );
186    assert!(
187        resolved_with_context == context,
188        "Field Context must win when no explicit binding is present"
189    );
190    assert_eq!(
191        internal_value, expected_internal_value,
192        "internal state must be used when neither an explicit binding nor Field Context is present"
193    );
194}
195
196/// Asserts explicit metadata, context metadata, then standalone metadata resolution precedence.
197///
198/// # Panics
199///
200/// Panics when either resolved metadata handle has the wrong identity or the standalone flags do
201/// not match the expected defaults.
202pub fn assert_meta_resolution_precedence(
203    resolved_with_explicit: FieldMeta,
204    explicit: FieldMeta,
205    resolved_with_context: FieldMeta,
206    context: FieldMeta,
207    standalone_flags: OverridableMetaFlags,
208    expected_standalone_flags: OverridableMetaFlags,
209) {
210    assert!(
211        resolved_with_explicit == explicit,
212        "explicit metadata must win over Field Context"
213    );
214    assert!(
215        resolved_with_context == context,
216        "Field Context metadata must win when explicit metadata is absent"
217    );
218    assert_eq!(
219        standalone_flags, expected_standalone_flags,
220        "standalone metadata must be used when neither explicit metadata nor Field Context is present"
221    );
222}
223
224/// Asserts the invalid and disabled flags observed after applying explicit per-flag props.
225///
226/// Registry tests should obtain `observed` from the actual attributes or state rendered by their
227/// widget, not by recomputing metadata resolution in the test.
228///
229/// # Panics
230///
231/// Panics when either observed flag differs from the expected explicit-or-metadata result.
232pub fn assert_meta_flag_precedence(observed: OverridableMetaFlags, expected: OverridableMetaFlags) {
233    assert_eq!(
234        observed, expected,
235        "each explicit metadata flag must override only its corresponding metadata flag"
236    );
237}
238
239/// Records focus callbacks reached through a widget's resolved [`crate::FocusRequest`].
240#[derive(Clone, Debug, Default)]
241pub struct FocusRoundTripProbe {
242    focus_calls: Rc<Cell<usize>>,
243}
244
245impl FocusRoundTripProbe {
246    /// Creates an empty focus probe.
247    pub fn new() -> Self {
248        Self::default()
249    }
250
251    /// Returns the callback the widget should register for its actual control.
252    pub fn on_focus(&self) -> Callback<()> {
253        let focus_calls = Rc::clone(&self.focus_calls);
254        Callback::new(move |()| focus_calls.set(focus_calls.get() + 1))
255    }
256
257    /// Asserts that one producer focus request reached the widget control callback.
258    ///
259    /// # Panics
260    ///
261    /// Panics when the focus callback was omitted or invoked more than once.
262    pub fn assert_focus_round_trip(&self) {
263        assert_eq!(
264            self.focus_calls.get(),
265            1,
266            "one producer focus request must reach the widget's control exactly once"
267        );
268    }
269}
270
271impl PartialEq for FocusRoundTripProbe {
272    fn eq(&self, other: &Self) -> bool {
273        Rc::ptr_eq(&self.focus_calls, &other.focus_calls)
274    }
275}
276
277/// Asserts the description and error ids currently registered in field metadata.
278///
279/// Call this once after the registry's description and error parts mount, then again with empty
280/// expected slices after they drop. Id order must match mount order because ARIA id references are
281/// rendered in registration order.
282///
283/// # Panics
284///
285/// Panics when the metadata's ARIA id references differ from the expected ids.
286pub fn assert_field_part_ids(
287    meta: FieldMeta,
288    expected_description_ids: &[&str],
289    expected_error_ids: &[&str],
290) {
291    let attributes = meta.attributes_with(FieldMetaOverrides {
292        invalid: Some(true),
293        disabled: None,
294    });
295    assert_eq!(
296        attribute_text(&attributes, "aria-describedby"),
297        joined_ids(expected_description_ids),
298        "description ids must match the currently mounted description parts"
299    );
300    assert_eq!(
301        attribute_text(&attributes, "aria-errormessage"),
302        joined_ids(expected_error_ids),
303        "error ids must match the currently mounted error parts"
304    );
305}
306
307fn attribute_text(attributes: &[Attribute], name: &str) -> Option<String> {
308    attributes
309        .iter()
310        .find(|attribute| attribute.name == name)
311        .and_then(|attribute| match &attribute.value {
312            AttributeValue::Text(value) => Some(value.clone()),
313            _ => None,
314        })
315}
316
317fn joined_ids(ids: &[&str]) -> Option<String> {
318    (!ids.is_empty()).then(|| ids.join(" "))
319}