use std::rc::Rc;
use yew::prelude::*;
use yew_router::prelude::*;
pub trait BreadcrumbLabelProvider: Send + Sync {
fn label_for_path(&self, path: &str) -> String;
}
#[derive(Clone)]
struct BreadcrumbLabelProviderContext(Rc<dyn BreadcrumbLabelProvider>);
impl PartialEq for BreadcrumbLabelProviderContext {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BreadcrumbItem<R> {
pub route: R,
pub label: String,
pub is_active: bool
}
#[hook]
pub fn use_breadcrumbs<R>() -> Vec<BreadcrumbItem<R>>
where
R: Routable + Clone + PartialEq + 'static
{
let current = use_route::<R>();
let provider = use_context::<BreadcrumbLabelProviderContext>();
current.map_or_else(std::vec::Vec::new, |route| {
let path = route.to_path();
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let mut items = Vec::new();
let mut built = String::new();
let root_label = provider
.as_ref()
.map_or_else(|| "/".to_string(), |p| p.0.label_for_path("/"));
items.push(BreadcrumbItem {
route: route.clone(),
label: root_label,
is_active: segments.is_empty()
});
let is_last = segments.len();
for (i, segment) in segments.iter().enumerate() {
built.push('/');
built.push_str(segment);
let is_last = i + 1 == is_last;
let label = provider
.as_ref()
.map_or_else(|| built.clone(), |p| p.0.label_for_path(&built));
items.push(BreadcrumbItem {
route: route.clone(),
label,
is_active: is_last
});
}
items
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, PartialEq, Debug, Routable)]
enum SimpleRoute {
#[at("/")]
Home,
#[at("/about")]
About,
#[at("/docs")]
Docs,
#[at("/docs/api")]
Api,
#[at("/docs/api/v1")]
ApiV1
}
#[derive(Clone, PartialEq, Debug, Routable)]
enum ParamRoute {
#[at("/")]
Home,
#[at("/users/:id")]
User { id: String }
}
#[derive(Clone, PartialEq, Debug, Routable)]
enum RootOnlyRoute {
#[at("/")]
Root
}
struct TestLabelProvider;
impl BreadcrumbLabelProvider for TestLabelProvider {
fn label_for_path(&self, path: &str) -> String {
match path {
"/" => "Home".to_string(),
"/about" => "About".to_string(),
"/docs" => "Docs".to_string(),
"/docs/api" => "API".to_string(),
"/docs/api/v1" => "V1".to_string(),
"/users/42" => "User #42".to_string(),
_ => path.to_string()
}
}
}
#[test]
fn breadcrumb_item_new() {
let item = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
assert_eq!(item.label, "Home");
assert!(item.is_active);
assert_eq!(item.route.to_path(), "/");
}
#[test]
fn breadcrumb_item_inactive() {
let item = BreadcrumbItem {
route: SimpleRoute::About,
label: "About".to_string(),
is_active: false
};
assert!(!item.is_active);
assert_eq!(item.label, "About");
}
#[test]
fn breadcrumb_item_clone_preserves_all_fields() {
let item1 = BreadcrumbItem {
route: SimpleRoute::Api,
label: "Root".to_string(),
is_active: true
};
let item2 = item1.clone();
assert_eq!(item1, item2);
assert_eq!(item2.label, "Root");
assert!(item2.is_active);
}
#[test]
fn breadcrumb_item_eq_with_same_values() {
let item1 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
let item2 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
assert_eq!(item1, item2);
}
#[test]
fn breadcrumb_item_neq_different_label() {
let item1 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
let item2 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Index".to_string(),
is_active: true
};
assert_ne!(item1, item2);
}
#[test]
fn breadcrumb_item_neq_different_state() {
let item1 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
let item2 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: false
};
assert_ne!(item1, item2);
}
#[test]
fn breadcrumb_item_neq_different_route() {
let item1 = BreadcrumbItem {
route: SimpleRoute::Docs,
label: "Docs".to_string(),
is_active: false
};
let item2 = BreadcrumbItem {
route: SimpleRoute::Api,
label: "Docs".to_string(),
is_active: false
};
assert_ne!(item1, item2);
}
#[test]
fn breadcrumb_item_debug_contains_all_fields() {
let item = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
let debug_str = format!("{item:?}");
assert!(debug_str.contains("BreadcrumbItem"));
assert!(debug_str.contains("Home"));
assert!(debug_str.contains("is_active"));
}
#[test]
fn breadcrumb_item_long_label() {
let label = "Extremely long breadcrumb label to test string handling in various scenarios"
.to_string();
let item = BreadcrumbItem {
route: SimpleRoute::Home,
label: label.clone(),
is_active: false
};
assert_eq!(item.label, label);
assert!(!item.is_active);
}
#[test]
fn breadcrumb_item_short_label() {
let item = BreadcrumbItem {
route: SimpleRoute::Home,
label: "a".to_string(),
is_active: true
};
assert_eq!(item.label, "a");
}
#[test]
fn breadcrumb_item_clone_deep_copy() {
let item1 = BreadcrumbItem {
route: SimpleRoute::ApiV1,
label: "Deep".to_string(),
is_active: true
};
let item2 = item1.clone();
assert_eq!(item1, item2);
}
#[test]
fn breadcrumb_item_root_path() {
let item = BreadcrumbItem {
route: SimpleRoute::Home,
label: "/".to_string(),
is_active: true
};
assert_eq!(item.route.to_path(), "/");
}
#[test]
fn breadcrumb_item_nested_path() {
let item = BreadcrumbItem {
route: SimpleRoute::ApiV1,
label: "/docs/api/v1".to_string(),
is_active: true
};
assert_eq!(item.route.to_path(), "/docs/api/v1");
}
#[test]
fn breadcrumb_label_provider_returns_custom_labels() {
let provider = TestLabelProvider;
assert_eq!(provider.label_for_path("/"), "Home");
assert_eq!(provider.label_for_path("/about"), "About");
assert_eq!(provider.label_for_path("/docs/api/v1"), "V1");
}
#[test]
fn breadcrumb_label_provider_returns_path_for_unknown() {
let provider = TestLabelProvider;
assert_eq!(provider.label_for_path("/unknown/path"), "/unknown/path");
assert_eq!(provider.label_for_path("/missing"), "/missing");
}
#[test]
fn breadcrumb_label_provider_empty_path_not_root() {
let provider = TestLabelProvider;
assert_ne!(provider.label_for_path(""), "Home");
}
#[test]
fn breadcrumb_label_provider_whitespace() {
let provider = TestLabelProvider;
assert_eq!(provider.label_for_path(" "), " ");
}
#[test]
fn breadcrumb_label_provider_special_chars() {
let provider = TestLabelProvider;
assert_eq!(provider.label_for_path("@#$%"), "@#$%");
}
#[test]
fn context_eq_same_rc() {
let rc = Rc::new(TestLabelProvider);
let ctx1 = BreadcrumbLabelProviderContext(rc.clone());
let ctx2 = BreadcrumbLabelProviderContext(rc);
assert!(ctx1 == ctx2);
}
#[test]
fn context_neq_different_rc() {
let ctx1 = BreadcrumbLabelProviderContext(Rc::new(TestLabelProvider));
let ctx2 = BreadcrumbLabelProviderContext(Rc::new(TestLabelProvider));
assert!(ctx1 != ctx2);
}
#[test]
fn context_clone_preserves_identity() {
let rc = Rc::new(TestLabelProvider);
let ctx1 = BreadcrumbLabelProviderContext(rc);
let ctx2 = ctx1.clone();
assert!(ctx1 == ctx2);
}
#[test]
fn use_breadcrumbs_simple_route() {
let _result = use_breadcrumbs::<SimpleRoute>();
}
#[test]
fn use_breadcrumbs_param_route() {
let _result = use_breadcrumbs::<ParamRoute>();
}
#[test]
fn use_breadcrumbs_multiple_calls() {
let _r1 = use_breadcrumbs::<SimpleRoute>();
let _r2 = use_breadcrumbs::<SimpleRoute>();
let _r3 = use_breadcrumbs::<SimpleRoute>();
}
#[test]
fn use_breadcrumbs_root_only_route() {
let _result = use_breadcrumbs::<RootOnlyRoute>();
}
#[test]
fn use_breadcrumbs_all_route_types() {
let _simple = use_breadcrumbs::<SimpleRoute>();
let _param = use_breadcrumbs::<ParamRoute>();
let _root = use_breadcrumbs::<RootOnlyRoute>();
}
#[test]
fn breadcrumb_item_neq_negatives() {
let item1 = BreadcrumbItem {
route: SimpleRoute::Home,
label: "Home".to_string(),
is_active: true
};
let mut item2 = item1.clone();
item2.label = "Other".to_string();
assert_ne!(item1, item2);
item2.is_active = false;
assert_ne!(item1, item2);
}
}