1use 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#[derive(Clone, Debug, Default)]
41pub struct CommitOrderProbe {
42 events: Rc<RefCell<Vec<CommitOrderEvent>>>,
43}
44
45impl CommitOrderProbe {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 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 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 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#[derive(Debug)]
85pub struct ChangeOriginProbe<T> {
86 writes: Rc<RefCell<Vec<(T, ChangeOrigin)>>>,
87}
88
89impl<T> ChangeOriginProbe<T> {
90 pub fn new() -> Self {
92 Self::default()
93 }
94}
95
96impl<T: 'static> ChangeOriginProbe<T> {
97 pub fn binding(&self, read: ReadSignal<T>) -> Binding<T> {
99 self.binding_with_commit(read, Callback::new(|()| {}))
100 }
101
102 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub struct OverridableMetaFlags {
148 pub invalid: bool,
150 pub disabled: bool,
152}
153
154impl OverridableMetaFlags {
155 pub const fn new(invalid: bool, disabled: bool) -> Self {
157 Self { invalid, disabled }
158 }
159}
160
161#[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
196pub 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
224pub 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#[derive(Clone, Debug, Default)]
241pub struct FocusRoundTripProbe {
242 focus_calls: Rc<Cell<usize>>,
243}
244
245impl FocusRoundTripProbe {
246 pub fn new() -> Self {
248 Self::default()
249 }
250
251 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 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
277pub 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}