use std::collections::HashMap;
use crate::action::Action;
use crate::component::{Component, ComponentNode};
use crate::view::JsonUiView;
fn resolve_action(action: &mut Action, resolver: &impl Fn(&str) -> Option<String>) {
if action.url.is_none() {
if action.handler.starts_with('/') {
action.url = Some(action.handler.clone());
return;
}
if let Some(url) = resolver(&action.handler) {
action.url = Some(url);
}
}
}
fn resolve_component_node(node: &mut ComponentNode, resolver: &impl Fn(&str) -> Option<String>) {
if let Some(ref mut action) = node.action {
resolve_action(action, resolver);
}
match &mut node.component {
Component::Card(props) => {
for child in &mut props.children {
resolve_component_node(child, resolver);
}
for child in &mut props.footer {
resolve_component_node(child, resolver);
}
}
Component::Form(props) => {
resolve_action(&mut props.action, resolver);
for field in &mut props.fields {
resolve_component_node(field, resolver);
}
}
Component::Modal(props) => {
for child in &mut props.children {
resolve_component_node(child, resolver);
}
for child in &mut props.footer {
resolve_component_node(child, resolver);
}
}
Component::Tabs(props) => {
for tab in &mut props.tabs {
for child in &mut tab.children {
resolve_component_node(child, resolver);
}
}
}
Component::Table(props) => {
if let Some(ref mut row_actions) = props.row_actions {
for action in row_actions {
resolve_action(action, resolver);
}
}
}
Component::Grid(props) => {
for child in &mut props.children {
resolve_component_node(child, resolver);
}
}
Component::Collapsible(props) => {
for child in &mut props.children {
resolve_component_node(child, resolver);
}
}
Component::FormSection(props) => {
for child in &mut props.children {
resolve_component_node(child, resolver);
}
}
Component::PageHeader(props) => {
for child in &mut props.actions {
resolve_component_node(child, resolver);
}
}
Component::ButtonGroup(props) => {
for child in &mut props.buttons {
resolve_component_node(child, resolver);
}
}
Component::DropdownMenu(props) => {
for item in &mut props.items {
resolve_action(&mut item.action, resolver);
}
}
Component::KanbanBoard(props) => {
for col in &mut props.columns {
for child in &mut col.children {
resolve_component_node(child, resolver);
}
}
}
Component::EmptyState(props) => {
if let Some(ref mut action) = props.action {
resolve_action(action, resolver);
}
}
Component::Switch(props) => {
if let Some(ref mut action) = props.action {
resolve_action(action, resolver);
}
}
Component::DataTable(props) => {
if let Some(ref mut actions) = props.row_actions {
for item in actions {
resolve_action(&mut item.action, resolver);
}
}
}
Component::Button(_)
| Component::Input(_)
| Component::Select(_)
| Component::Alert(_)
| Component::Badge(_)
| Component::Text(_)
| Component::Checkbox(_)
| Component::Separator(_)
| Component::DescriptionList(_)
| Component::Breadcrumb(_)
| Component::Pagination(_)
| Component::Progress(_)
| Component::Avatar(_)
| Component::Skeleton(_)
| Component::StatCard(_)
| Component::Checklist(_)
| Component::Toast(_)
| Component::NotificationDropdown(_)
| Component::Sidebar(_)
| Component::Header(_)
| Component::CalendarCell(_)
| Component::ActionCard(_)
| Component::ProductTile(_)
| Component::Image(_)
| Component::KeyValueEditor(_)
| Component::Plugin(_) => {}
}
}
pub fn resolve_actions(view: &mut JsonUiView, resolver: impl Fn(&str) -> Option<String>) {
for node in &mut view.components {
resolve_component_node(node, &resolver);
}
}
pub fn resolve_actions_strict(
view: &mut JsonUiView,
resolver: impl Fn(&str) -> Option<String>,
) -> Result<(), Vec<String>> {
let mut unresolved: Vec<String> = Vec::new();
let collecting_resolver = |handler: &str| -> Option<String> { resolver(handler) };
resolve_actions(view, collecting_resolver);
for node in &view.components {
collect_unresolved_node(node, &mut unresolved);
}
if unresolved.is_empty() {
Ok(())
} else {
Err(unresolved)
}
}
fn collect_unresolved_action(action: &Action, unresolved: &mut Vec<String>) {
if action.url.is_none() {
unresolved.push(action.handler.clone());
}
}
fn collect_unresolved_node(node: &ComponentNode, unresolved: &mut Vec<String>) {
if let Some(ref action) = node.action {
collect_unresolved_action(action, unresolved);
}
match &node.component {
Component::Card(props) => {
for child in &props.children {
collect_unresolved_node(child, unresolved);
}
for child in &props.footer {
collect_unresolved_node(child, unresolved);
}
}
Component::Form(props) => {
collect_unresolved_action(&props.action, unresolved);
for field in &props.fields {
collect_unresolved_node(field, unresolved);
}
}
Component::Modal(props) => {
for child in &props.children {
collect_unresolved_node(child, unresolved);
}
for child in &props.footer {
collect_unresolved_node(child, unresolved);
}
}
Component::Tabs(props) => {
for tab in &props.tabs {
for child in &tab.children {
collect_unresolved_node(child, unresolved);
}
}
}
Component::Table(props) => {
if let Some(ref row_actions) = props.row_actions {
for action in row_actions {
collect_unresolved_action(action, unresolved);
}
}
}
Component::Grid(props) => {
for child in &props.children {
collect_unresolved_node(child, unresolved);
}
}
Component::Collapsible(props) => {
for child in &props.children {
collect_unresolved_node(child, unresolved);
}
}
Component::FormSection(props) => {
for child in &props.children {
collect_unresolved_node(child, unresolved);
}
}
Component::PageHeader(props) => {
for child in &props.actions {
collect_unresolved_node(child, unresolved);
}
}
Component::ButtonGroup(props) => {
for child in &props.buttons {
collect_unresolved_node(child, unresolved);
}
}
Component::DropdownMenu(props) => {
for item in &props.items {
collect_unresolved_action(&item.action, unresolved);
}
}
Component::KanbanBoard(props) => {
for col in &props.columns {
for child in &col.children {
collect_unresolved_node(child, unresolved);
}
}
}
Component::EmptyState(props) => {
if let Some(ref action) = props.action {
collect_unresolved_action(action, unresolved);
}
}
Component::Switch(props) => {
if let Some(ref action) = props.action {
collect_unresolved_action(action, unresolved);
}
}
Component::DataTable(props) => {
if let Some(ref actions) = props.row_actions {
for item in actions {
collect_unresolved_action(&item.action, unresolved);
}
}
}
Component::Button(_)
| Component::Input(_)
| Component::Select(_)
| Component::Alert(_)
| Component::Badge(_)
| Component::Text(_)
| Component::Checkbox(_)
| Component::Separator(_)
| Component::DescriptionList(_)
| Component::Breadcrumb(_)
| Component::Pagination(_)
| Component::Progress(_)
| Component::Avatar(_)
| Component::Skeleton(_)
| Component::StatCard(_)
| Component::Checklist(_)
| Component::Toast(_)
| Component::NotificationDropdown(_)
| Component::Sidebar(_)
| Component::Header(_)
| Component::CalendarCell(_)
| Component::ActionCard(_)
| Component::ProductTile(_)
| Component::Image(_)
| Component::KeyValueEditor(_)
| Component::Plugin(_) => {}
}
}
pub fn resolve_errors(view: &mut JsonUiView, errors: &HashMap<String, Vec<String>>) {
for node in &mut view.components {
resolve_errors_node(node, errors, false);
}
}
pub fn resolve_errors_all(view: &mut JsonUiView, errors: &HashMap<String, Vec<String>>) {
for node in &mut view.components {
resolve_errors_node(node, errors, true);
}
}
fn set_field_error(
error_slot: &mut Option<String>,
field: &str,
errors: &HashMap<String, Vec<String>>,
all: bool,
) {
if error_slot.is_some() {
return; }
if let Some(messages) = errors.get(field) {
if !messages.is_empty() {
if all {
*error_slot = Some(messages.join(". "));
} else {
*error_slot = Some(messages[0].clone());
}
}
}
}
fn resolve_errors_node(node: &mut ComponentNode, errors: &HashMap<String, Vec<String>>, all: bool) {
match &mut node.component {
Component::Input(props) => {
set_field_error(&mut props.error, &props.field, errors, all);
}
Component::Select(props) => {
set_field_error(&mut props.error, &props.field, errors, all);
}
Component::Checkbox(props) => {
set_field_error(&mut props.error, &props.field, errors, all);
}
Component::Switch(props) => {
set_field_error(&mut props.error, &props.field, errors, all);
}
Component::Card(props) => {
for child in &mut props.children {
resolve_errors_node(child, errors, all);
}
for child in &mut props.footer {
resolve_errors_node(child, errors, all);
}
}
Component::Form(props) => {
for field in &mut props.fields {
resolve_errors_node(field, errors, all);
}
}
Component::Modal(props) => {
for child in &mut props.children {
resolve_errors_node(child, errors, all);
}
for child in &mut props.footer {
resolve_errors_node(child, errors, all);
}
}
Component::Tabs(props) => {
for tab in &mut props.tabs {
for child in &mut tab.children {
resolve_errors_node(child, errors, all);
}
}
}
Component::Grid(props) => {
for child in &mut props.children {
resolve_errors_node(child, errors, all);
}
}
Component::Collapsible(props) => {
for child in &mut props.children {
resolve_errors_node(child, errors, all);
}
}
Component::FormSection(props) => {
for child in &mut props.children {
resolve_errors_node(child, errors, all);
}
}
Component::PageHeader(props) => {
for child in &mut props.actions {
resolve_errors_node(child, errors, all);
}
}
Component::ButtonGroup(props) => {
for child in &mut props.buttons {
resolve_errors_node(child, errors, all);
}
}
Component::Table(_)
| Component::Button(_)
| Component::Alert(_)
| Component::Badge(_)
| Component::Text(_)
| Component::Separator(_)
| Component::DescriptionList(_)
| Component::Breadcrumb(_)
| Component::Pagination(_)
| Component::Progress(_)
| Component::Avatar(_)
| Component::Skeleton(_)
| Component::StatCard(_)
| Component::Checklist(_)
| Component::Toast(_)
| Component::NotificationDropdown(_)
| Component::Sidebar(_)
| Component::Header(_)
| Component::EmptyState(_)
| Component::DropdownMenu(_)
| Component::KanbanBoard(_)
| Component::CalendarCell(_)
| Component::ActionCard(_)
| Component::ProductTile(_)
| Component::DataTable(_)
| Component::Image(_)
| Component::Plugin(_) => {}
Component::KeyValueEditor(props) => {
set_field_error(&mut props.error, &props.field, errors, all);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::HttpMethod;
use crate::component::*;
fn make_action(handler: &str) -> Action {
Action {
handler: handler.to_string(),
url: None,
method: HttpMethod::Post,
confirm: None,
on_success: None,
on_error: None,
target: None,
}
}
fn test_resolver(handler: &str) -> Option<String> {
match handler {
"users.store" => Some("/users".to_string()),
"users.show" => Some("/users/{id}".to_string()),
"users.destroy" => Some("/users/{id}".to_string()),
"users.create" => Some("/users/create".to_string()),
"posts.index" => Some("/posts".to_string()),
_ => None,
}
}
#[test]
fn resolve_button_with_action() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "btn".to_string(),
component: Component::Button(ButtonProps {
label: "Create".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.store")),
visibility: None,
});
resolve_actions(&mut view, test_resolver);
assert_eq!(
view.components[0].action.as_ref().unwrap().url,
Some("/users".to_string())
);
}
#[test]
fn resolve_nested_card_children() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "card".to_string(),
component: Component::Card(CardProps {
title: "Users".to_string(),
description: None,
max_width: None,
children: vec![ComponentNode {
key: "btn".to_string(),
component: Component::Button(ButtonProps {
label: "Create".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.create")),
visibility: None,
}],
footer: vec![ComponentNode {
key: "footer-btn".to_string(),
component: Component::Button(ButtonProps {
label: "Save".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.store")),
visibility: None,
}],
}),
action: None,
visibility: None,
});
resolve_actions(&mut view, test_resolver);
match &view.components[0].component {
Component::Card(props) => {
assert_eq!(
props.children[0].action.as_ref().unwrap().url,
Some("/users/create".to_string())
);
assert_eq!(
props.footer[0].action.as_ref().unwrap().url,
Some("/users".to_string())
);
}
_ => panic!("expected Card"),
}
}
#[test]
fn resolve_form_action() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "form".to_string(),
component: Component::Form(FormProps {
action: make_action("users.store"),
fields: vec![ComponentNode {
key: "name".to_string(),
component: Component::Input(InputProps {
field: "name".to_string(),
label: "Name".to_string(),
input_type: InputType::Text,
placeholder: None,
required: None,
disabled: None,
error: None,
description: None,
default_value: None,
data_path: None,
step: None,
list: None,
}),
action: None,
visibility: None,
}],
method: None,
guard: None,
max_width: None,
}),
action: None,
visibility: None,
});
resolve_actions(&mut view, test_resolver);
match &view.components[0].component {
Component::Form(props) => {
assert_eq!(props.action.url, Some("/users".to_string()));
}
_ => panic!("expected Form"),
}
}
#[test]
fn resolve_table_row_actions() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "table".to_string(),
component: Component::Table(TableProps {
columns: vec![Column {
key: "name".to_string(),
label: "Name".to_string(),
format: None,
}],
data_path: "/data/users".to_string(),
row_actions: Some(vec![
make_action("users.show"),
make_action("users.destroy"),
]),
empty_message: None,
sortable: None,
sort_column: None,
sort_direction: None,
}),
action: None,
visibility: None,
});
resolve_actions(&mut view, test_resolver);
match &view.components[0].component {
Component::Table(props) => {
let row_actions = props.row_actions.as_ref().unwrap();
assert_eq!(row_actions[0].url, Some("/users/{id}".to_string()));
assert_eq!(row_actions[1].url, Some("/users/{id}".to_string()));
}
_ => panic!("expected Table"),
}
}
#[test]
fn resolve_tabs_children() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "tabs".to_string(),
component: Component::Tabs(TabsProps {
default_tab: "general".to_string(),
tabs: vec![
Tab {
value: "general".to_string(),
label: "General".to_string(),
children: vec![ComponentNode {
key: "btn1".to_string(),
component: Component::Button(ButtonProps {
label: "Save".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.store")),
visibility: None,
}],
},
Tab {
value: "posts".to_string(),
label: "Posts".to_string(),
children: vec![ComponentNode {
key: "btn2".to_string(),
component: Component::Button(ButtonProps {
label: "View Posts".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("posts.index")),
visibility: None,
}],
},
],
}),
action: None,
visibility: None,
});
resolve_actions(&mut view, test_resolver);
match &view.components[0].component {
Component::Tabs(props) => {
assert_eq!(
props.tabs[0].children[0].action.as_ref().unwrap().url,
Some("/users".to_string())
);
assert_eq!(
props.tabs[1].children[0].action.as_ref().unwrap().url,
Some("/posts".to_string())
);
}
_ => panic!("expected Tabs"),
}
}
#[test]
fn resolve_modal_children_and_footer() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "modal".to_string(),
component: Component::Modal(ModalProps {
id: "modal-confirm".to_string(),
title: "Confirm".to_string(),
description: None,
children: vec![ComponentNode {
key: "info".to_string(),
component: Component::Text(TextProps {
content: "Are you sure?".to_string(),
element: TextElement::P,
}),
action: None,
visibility: None,
}],
footer: vec![ComponentNode {
key: "confirm-btn".to_string(),
component: Component::Button(ButtonProps {
label: "Delete".to_string(),
variant: ButtonVariant::Destructive,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.destroy")),
visibility: None,
}],
trigger_label: Some("Open".to_string()),
}),
action: None,
visibility: None,
});
resolve_actions(&mut view, test_resolver);
match &view.components[0].component {
Component::Modal(props) => {
assert_eq!(
props.footer[0].action.as_ref().unwrap().url,
Some("/users/{id}".to_string())
);
}
_ => panic!("expected Modal"),
}
}
#[test]
fn unresolvable_handler_leaves_url_none() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "btn".to_string(),
component: Component::Button(ButtonProps {
label: "Unknown".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("nonexistent.handler")),
visibility: None,
});
resolve_actions(&mut view, test_resolver);
assert_eq!(view.components[0].action.as_ref().unwrap().url, None);
}
#[test]
fn strict_with_missing_handler_returns_error() {
let mut view = JsonUiView::new()
.component(ComponentNode {
key: "btn1".to_string(),
component: Component::Button(ButtonProps {
label: "OK".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.store")),
visibility: None,
})
.component(ComponentNode {
key: "btn2".to_string(),
component: Component::Button(ButtonProps {
label: "Bad".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("unknown.handler")),
visibility: None,
});
let result = resolve_actions_strict(&mut view, test_resolver);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors, vec!["unknown.handler"]);
assert_eq!(
view.components[0].action.as_ref().unwrap().url,
Some("/users".to_string())
);
}
#[test]
fn strict_with_all_resolved_returns_ok() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "btn".to_string(),
component: Component::Button(ButtonProps {
label: "Create".to_string(),
variant: ButtonVariant::Default,
size: Size::Default,
disabled: None,
icon: None,
icon_position: None,
button_type: None,
}),
action: Some(make_action("users.store")),
visibility: None,
});
let result = resolve_actions_strict(&mut view, test_resolver);
assert!(result.is_ok());
}
fn make_errors(pairs: &[(&str, &[&str])]) -> HashMap<String, Vec<String>> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.iter().map(|s| s.to_string()).collect()))
.collect()
}
fn make_input_node(key: &str, field: &str) -> ComponentNode {
ComponentNode {
key: key.to_string(),
component: Component::Input(InputProps {
field: field.to_string(),
label: field.to_string(),
input_type: InputType::Text,
placeholder: None,
required: None,
disabled: None,
error: None,
description: None,
default_value: None,
data_path: None,
step: None,
list: None,
}),
action: None,
visibility: None,
}
}
#[test]
fn resolve_errors_populates_input_error() {
let mut view = JsonUiView::new().component(make_input_node("email-input", "email"));
let errors = make_errors(&[("email", &["Email is required"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Input(props) => {
assert_eq!(props.error, Some("Email is required".to_string()));
}
_ => panic!("expected Input"),
}
}
#[test]
fn resolve_errors_populates_select_error() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "role-select".to_string(),
component: Component::Select(SelectProps {
field: "role".to_string(),
label: "Role".to_string(),
options: vec![SelectOption {
value: "admin".to_string(),
label: "Admin".to_string(),
}],
placeholder: None,
required: None,
disabled: None,
error: None,
description: None,
default_value: None,
data_path: None,
}),
action: None,
visibility: None,
});
let errors = make_errors(&[("role", &["Role is required"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Select(props) => {
assert_eq!(props.error, Some("Role is required".to_string()));
}
_ => panic!("expected Select"),
}
}
#[test]
fn resolve_errors_populates_checkbox_error() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "terms-checkbox".to_string(),
component: Component::Checkbox(CheckboxProps {
field: "terms".to_string(),
value: None,
label: "Accept Terms".to_string(),
description: None,
checked: None,
data_path: None,
required: None,
disabled: None,
error: None,
}),
action: None,
visibility: None,
});
let errors = make_errors(&[("terms", &["You must accept the terms"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Checkbox(props) => {
assert_eq!(props.error, Some("You must accept the terms".to_string()));
}
_ => panic!("expected Checkbox"),
}
}
#[test]
fn resolve_errors_populates_switch_error() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "notif-switch".to_string(),
component: Component::Switch(SwitchProps {
field: "notifications".to_string(),
label: "Notifications".to_string(),
description: None,
checked: None,
data_path: None,
required: None,
disabled: None,
error: None,
action: None,
compact: false,
}),
action: None,
visibility: None,
});
let errors = make_errors(&[("notifications", &["Must enable notifications"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Switch(props) => {
assert_eq!(props.error, Some("Must enable notifications".to_string()));
}
_ => panic!("expected Switch"),
}
}
#[test]
fn resolve_errors_does_not_overwrite_existing() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "email-input".to_string(),
component: Component::Input(InputProps {
field: "email".to_string(),
label: "Email".to_string(),
input_type: InputType::Email,
placeholder: None,
required: None,
disabled: None,
error: Some("Custom error".to_string()),
description: None,
default_value: None,
data_path: None,
step: None,
list: None,
}),
action: None,
visibility: None,
});
let errors = make_errors(&[("email", &["Validation error"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Input(props) => {
assert_eq!(props.error, Some("Custom error".to_string()));
}
_ => panic!("expected Input"),
}
}
#[test]
fn resolve_errors_nested_in_form() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "form".to_string(),
component: Component::Form(FormProps {
action: make_action("users.store"),
fields: vec![
make_input_node("name-input", "name"),
make_input_node("email-input", "email"),
],
method: None,
guard: None,
max_width: None,
}),
action: None,
visibility: None,
});
let errors = make_errors(&[
("name", &["Name is required"]),
("email", &["Email is invalid"]),
]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Form(props) => {
match &props.fields[0].component {
Component::Input(p) => {
assert_eq!(p.error, Some("Name is required".to_string()));
}
_ => panic!("expected Input"),
}
match &props.fields[1].component {
Component::Input(p) => {
assert_eq!(p.error, Some("Email is invalid".to_string()));
}
_ => panic!("expected Input"),
}
}
_ => panic!("expected Form"),
}
}
#[test]
fn resolve_errors_nested_in_card() {
let mut view = JsonUiView::new().component(ComponentNode {
key: "card".to_string(),
component: Component::Card(CardProps {
title: "User".to_string(),
description: None,
children: vec![make_input_node("name-input", "name")],
footer: vec![],
max_width: None,
}),
action: None,
visibility: None,
});
let errors = make_errors(&[("name", &["Name is required"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Card(props) => match &props.children[0].component {
Component::Input(p) => {
assert_eq!(p.error, Some("Name is required".to_string()));
}
_ => panic!("expected Input"),
},
_ => panic!("expected Card"),
}
}
#[test]
fn resolve_errors_no_matching_field() {
let mut view = JsonUiView::new().component(make_input_node("email-input", "email"));
let errors = make_errors(&[("unknown_field", &["Some error"])]);
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Input(props) => {
assert_eq!(props.error, None);
}
_ => panic!("expected Input"),
}
}
#[test]
fn resolve_errors_all_concatenates_messages() {
let mut view = JsonUiView::new().component(make_input_node("email-input", "email"));
let errors = make_errors(&[("email", &["Too short", "Invalid format", "Already taken"])]);
resolve_errors_all(&mut view, &errors);
match &view.components[0].component {
Component::Input(props) => {
assert_eq!(
props.error,
Some("Too short. Invalid format. Already taken".to_string())
);
}
_ => panic!("expected Input"),
}
}
#[test]
fn resolve_errors_empty_errors_map() {
let mut view = JsonUiView::new().component(make_input_node("email-input", "email"));
let errors: HashMap<String, Vec<String>> = HashMap::new();
resolve_errors(&mut view, &errors);
match &view.components[0].component {
Component::Input(props) => {
assert_eq!(props.error, None);
}
_ => panic!("expected Input"),
}
}
}