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, FieldControlOptions, FieldMeta};
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 pub required: bool,
154}
155
156impl OverridableMetaFlags {
157 pub const fn new(invalid: bool, disabled: bool) -> Self {
159 Self {
160 invalid,
161 disabled,
162 required: false,
163 }
164 }
165
166 #[must_use]
168 pub const fn with_required(mut self, required: bool) -> Self {
169 self.required = required;
170 self
171 }
172}
173
174#[allow(
184 clippy::needless_pass_by_value,
185 reason = "owned observed values keep the assertion API convenient for registry tests"
186)]
187pub fn assert_binding_resolution_precedence<T: Debug + PartialEq + 'static>(
188 resolved_with_explicit: &Binding<T>,
189 explicit: &Binding<T>,
190 resolved_with_context: &Binding<T>,
191 context: &Binding<T>,
192 internal_value: T,
193 expected_internal_value: T,
194) {
195 assert!(
196 resolved_with_explicit == explicit,
197 "an explicit binding must win over Field Context"
198 );
199 assert!(
200 resolved_with_context == context,
201 "Field Context must win when no explicit binding is present"
202 );
203 assert_eq!(
204 internal_value, expected_internal_value,
205 "internal state must be used when neither an explicit binding nor Field Context is present"
206 );
207}
208
209pub fn assert_meta_resolution_precedence(
216 resolved_with_explicit: FieldMeta,
217 explicit: FieldMeta,
218 resolved_with_context: FieldMeta,
219 context: FieldMeta,
220 standalone_flags: OverridableMetaFlags,
221 expected_standalone_flags: OverridableMetaFlags,
222) {
223 assert!(
224 resolved_with_explicit == explicit,
225 "explicit metadata must win over Field Context"
226 );
227 assert!(
228 resolved_with_context == context,
229 "Field Context metadata must win when explicit metadata is absent"
230 );
231 assert_eq!(
232 standalone_flags, expected_standalone_flags,
233 "standalone metadata must be used when neither explicit metadata nor Field Context is present"
234 );
235}
236
237pub fn assert_meta_flag_precedence(observed: OverridableMetaFlags, expected: OverridableMetaFlags) {
246 assert_eq!(
247 observed, expected,
248 "each explicit metadata flag must override only its corresponding metadata flag"
249 );
250}
251
252#[derive(Clone, Debug, Default)]
254pub struct FocusRoundTripProbe {
255 focus_calls: Rc<Cell<usize>>,
256}
257
258impl FocusRoundTripProbe {
259 pub fn new() -> Self {
261 Self::default()
262 }
263
264 pub fn on_focus(&self) -> Callback<()> {
266 let focus_calls = Rc::clone(&self.focus_calls);
267 Callback::new(move |()| focus_calls.set(focus_calls.get() + 1))
268 }
269
270 pub fn assert_focus_round_trip(&self) {
276 assert_eq!(
277 self.focus_calls.get(),
278 1,
279 "one producer focus request must reach the widget's control exactly once"
280 );
281 }
282
283 pub fn assert_focus_not_moved(&self) {
294 assert_eq!(
295 self.focus_calls.get(),
296 0,
297 "a focus request must not move focus while the control is disabled"
298 );
299 }
300}
301
302impl PartialEq for FocusRoundTripProbe {
303 fn eq(&self, other: &Self) -> bool {
304 Rc::ptr_eq(&self.focus_calls, &other.focus_calls)
305 }
306}
307
308pub fn assert_field_part_ids(
322 meta: FieldMeta,
323 expected_description_ids: &[&str],
324 expected_error_ids: &[&str],
325) {
326 let attributes = meta.attributes_for(&FieldControlOptions::new().invalid(Some(true)));
327 let mut expected_described_by = expected_description_ids.to_vec();
328 expected_described_by.extend_from_slice(expected_error_ids);
329
330 assert_eq!(
331 attribute_text(&attributes, "aria-describedby"),
332 joined_ids(&expected_described_by),
333 "description ids, then error ids, must match the currently mounted parts"
334 );
335 assert_eq!(
336 attribute_text(&attributes, "aria-errormessage").as_deref(),
337 expected_error_ids.first().copied(),
338 "aria-errormessage must reference the first mounted error part and nothing else"
339 );
340}
341
342fn attribute_text(attributes: &[Attribute], name: &str) -> Option<String> {
343 attributes
344 .iter()
345 .find(|attribute| attribute.name == name)
346 .and_then(|attribute| match &attribute.value {
347 AttributeValue::Text(value) => Some(value.clone()),
348 _ => None,
349 })
350}
351
352fn joined_ids(ids: &[&str]) -> Option<String> {
353 (!ids.is_empty()).then(|| ids.join(" "))
354}