use std::{
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
};
use dioxus_core::{Attribute, AttributeValue, Callback};
use dioxus_signals::ReadSignal;
use crate::{Binding, ChangeOrigin, FieldMeta, FieldMetaOverrides};
#[derive(Clone, Debug, Default)]
pub struct CommitOrderProbe {
events: Rc<RefCell<Vec<CommitOrderEvent>>>,
}
impl CommitOrderProbe {
pub fn new() -> Self {
Self::default()
}
pub fn on_commit(&self) -> Callback<()> {
let events = Rc::clone(&self.events);
Callback::new(move |()| events.borrow_mut().push(CommitOrderEvent::Commit))
}
pub fn on_submit(&self) -> Callback<()> {
let events = Rc::clone(&self.events);
Callback::new(move |()| events.borrow_mut().push(CommitOrderEvent::Submit))
}
pub fn assert_commit_before_submit(&self) {
assert_eq!(
*self.events.borrow(),
[CommitOrderEvent::Commit, CommitOrderEvent::Submit],
"the widget must synchronously commit exactly once before submit handling runs"
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CommitOrderEvent {
Commit,
Submit,
}
#[derive(Debug)]
pub struct ChangeOriginProbe<T> {
writes: Rc<RefCell<Vec<(T, ChangeOrigin)>>>,
}
impl<T> ChangeOriginProbe<T> {
pub fn new() -> Self {
Self::default()
}
}
impl<T: 'static> ChangeOriginProbe<T> {
pub fn binding(&self, read: ReadSignal<T>) -> Binding<T> {
self.binding_with_commit(read, Callback::new(|()| {}))
}
pub fn binding_with_commit(&self, read: ReadSignal<T>, on_commit: Callback<()>) -> Binding<T> {
let writes = Rc::clone(&self.writes);
Binding::new(
read,
Callback::new(move |write| writes.borrow_mut().push(write)),
on_commit,
)
}
}
impl<T: Debug + PartialEq> ChangeOriginProbe<T> {
pub fn assert_writes(&self, expected: &[(T, ChangeOrigin)]) {
assert_eq!(
self.writes.borrow().as_slice(),
expected,
"widget writes must retain their change origin"
);
}
}
impl<T> Clone for ChangeOriginProbe<T> {
fn clone(&self) -> Self {
Self {
writes: Rc::clone(&self.writes),
}
}
}
impl<T> Default for ChangeOriginProbe<T> {
fn default() -> Self {
Self {
writes: Rc::new(RefCell::new(Vec::new())),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OverridableMetaFlags {
pub invalid: bool,
pub disabled: bool,
}
impl OverridableMetaFlags {
pub const fn new(invalid: bool, disabled: bool) -> Self {
Self { invalid, disabled }
}
}
#[allow(
clippy::needless_pass_by_value,
reason = "owned observed values keep the assertion API convenient for registry tests"
)]
pub fn assert_binding_resolution_precedence<T: Debug + PartialEq + 'static>(
resolved_with_explicit: &Binding<T>,
explicit: &Binding<T>,
resolved_with_context: &Binding<T>,
context: &Binding<T>,
internal_value: T,
expected_internal_value: T,
) {
assert!(
resolved_with_explicit == explicit,
"an explicit binding must win over Field Context"
);
assert!(
resolved_with_context == context,
"Field Context must win when no explicit binding is present"
);
assert_eq!(
internal_value, expected_internal_value,
"internal state must be used when neither an explicit binding nor Field Context is present"
);
}
pub fn assert_meta_resolution_precedence(
resolved_with_explicit: FieldMeta,
explicit: FieldMeta,
resolved_with_context: FieldMeta,
context: FieldMeta,
standalone_flags: OverridableMetaFlags,
expected_standalone_flags: OverridableMetaFlags,
) {
assert!(
resolved_with_explicit == explicit,
"explicit metadata must win over Field Context"
);
assert!(
resolved_with_context == context,
"Field Context metadata must win when explicit metadata is absent"
);
assert_eq!(
standalone_flags, expected_standalone_flags,
"standalone metadata must be used when neither explicit metadata nor Field Context is present"
);
}
pub fn assert_meta_flag_precedence(observed: OverridableMetaFlags, expected: OverridableMetaFlags) {
assert_eq!(
observed, expected,
"each explicit metadata flag must override only its corresponding metadata flag"
);
}
#[derive(Clone, Debug, Default)]
pub struct FocusRoundTripProbe {
focus_calls: Rc<Cell<usize>>,
}
impl FocusRoundTripProbe {
pub fn new() -> Self {
Self::default()
}
pub fn on_focus(&self) -> Callback<()> {
let focus_calls = Rc::clone(&self.focus_calls);
Callback::new(move |()| focus_calls.set(focus_calls.get() + 1))
}
pub fn assert_focus_round_trip(&self) {
assert_eq!(
self.focus_calls.get(),
1,
"one producer focus request must reach the widget's control exactly once"
);
}
}
impl PartialEq for FocusRoundTripProbe {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.focus_calls, &other.focus_calls)
}
}
pub fn assert_field_part_ids(
meta: FieldMeta,
expected_description_ids: &[&str],
expected_error_ids: &[&str],
) {
let attributes = meta.attributes_with(FieldMetaOverrides {
invalid: Some(true),
disabled: None,
});
assert_eq!(
attribute_text(&attributes, "aria-describedby"),
joined_ids(expected_description_ids),
"description ids must match the currently mounted description parts"
);
assert_eq!(
attribute_text(&attributes, "aria-errormessage"),
joined_ids(expected_error_ids),
"error ids must match the currently mounted error parts"
);
}
fn attribute_text(attributes: &[Attribute], name: &str) -> Option<String> {
attributes
.iter()
.find(|attribute| attribute.name == name)
.and_then(|attribute| match &attribute.value {
AttributeValue::Text(value) => Some(value.clone()),
_ => None,
})
}
fn joined_ids(ids: &[&str]) -> Option<String> {
(!ids.is_empty()).then(|| ids.join(" "))
}