use crate::util::TestAttr;
use leptos::prelude::{
AriaAttributes, Children, ClassAttribute, CustomAttribute, ElementChild, Get, IntoView, Signal,
StyleAttribute, component, view,
};
fn base_class(extra: &str) -> String {
if extra.trim().is_empty() {
"message".to_string()
} else {
format!("message {}", extra)
}
}
#[component]
pub fn Message(
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
closable: Signal<bool>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
children: Children,
) -> impl IntoView {
let class = {
let classes = classes.clone();
move || base_class(&classes.get())
};
#[allow(unused)]
let (is_closed, set_is_closed) = leptos::prelude::signal(false);
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<article
class=class
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
style=move || {
let mut parts: Vec<&str> = Vec::new();
if closable.get() {
parts.push("position: relative;");
}
if is_closed.get() {
parts.push("display: none;");
}
parts.join(" ")
}
>
<button
class="delete is-small"
aria-label="delete"
type="button"
style=move || if closable.get() && !is_closed.get() {
"position:absolute; right:0.5rem; top:0.5rem; z-index: 10;"
} else {
"display: none;"
}
/>
{children()}
</article>
}
}
#[component]
pub fn MessageHeader(
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
children: Children,
) -> impl IntoView {
let class = {
let classes = classes.clone();
move || {
let extra = classes.get();
if extra.trim().is_empty() {
"message-header".to_string()
} else {
format!("message-header {}", extra)
}
}
};
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<div
class=class
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{children()}
</div>
}
}
#[component]
pub fn MessageBody(
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
children: Children,
) -> impl IntoView {
let class = {
let classes = classes.clone();
move || {
let extra = classes.get();
if extra.trim().is_empty() {
"message-body".to_string()
} else {
format!("message-body {}", extra)
}
}
};
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<div
class=class
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{children()}
</div>
}
}
#[cfg(test)]
mod tests {
use super::*;
use leptos::prelude::RenderHtml;
#[test]
fn message_renders_base_class_and_children() {
let html = view! {
<Message>
<p>"Hello"</p>
</Message>
}
.to_html();
assert!(
html.contains(r#"class="message""#),
"expected base 'message' class; got: {}",
html
);
assert!(
html.contains("Hello"),
"expected children rendered; got: {}",
html
);
}
#[test]
fn message_header_and_body_classes() {
let html = view! {
<Message classes="is-primary">
<MessageHeader><p>"Header"</p></MessageHeader>
<MessageBody><p>"Body"</p></MessageBody>
</Message>
}
.to_html();
assert!(
html.contains("message-header"),
"expected header class; got: {}",
html
);
assert!(
html.contains("message-body"),
"expected body class; got: {}",
html
);
assert!(
html.contains(r#"class="message is-primary""#) || html.contains("message is-primary "),
"expected color class on message; got: {}",
html
);
}
}
#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use super::*;
use leptos::prelude::*;
use std::rc::Rc;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
fn noop_close() -> Option<Rc<dyn Fn()>> {
Some(Rc::new(|| {}))
}
#[wasm_bindgen_test]
fn message_renders_test_attr_as_data_testid() {
let html = view! {
<Message classes="is-primary" test_attr="message-test">
<MessageBody><p>"Body"</p></MessageBody>
</Message>
}
.to_html();
assert!(
html.contains(r#"data-testid="message-test""#),
"expected data-testid attribute on Message; got: {}",
html
);
}
#[wasm_bindgen_test]
fn message_no_test_attr_when_not_provided() {
let html = view! {
<Message>
<MessageBody><p>"Body"</p></MessageBody>
</Message>
}
.to_html();
assert!(
!html.contains("data-testid") && !html.contains("data-cy"),
"expected no test attribute on Message when not provided; got: {}",
html
);
}
#[wasm_bindgen_test]
fn message_header_renders_test_attr_as_data_testid() {
let html = view! {
<MessageHeader classes="extra" test_attr="message-header-test">
<p>"Header"</p>
</MessageHeader>
}
.to_html();
assert!(
html.contains(r#"data-testid="message-header-test""#),
"expected data-testid attribute on MessageHeader; got: {}",
html
);
}
#[wasm_bindgen_test]
fn message_body_renders_test_attr_as_data_testid() {
let html = view! {
<MessageBody classes="extra" test_attr="message-body-test">
<p>"Body"</p>
</MessageBody>
}
.to_html();
assert!(
html.contains(r#"data-testid="message-body-test""#),
"expected data-testid attribute on MessageBody; got: {}",
html
);
}
#[wasm_bindgen_test]
fn message_closable_renders_delete_button() {
let html = view! {
<Message classes="is-primary" closable=true on_close=noop_close()>
<MessageBody><p>"Body"</p></MessageBody>
</Message>
}
.to_html();
assert!(
html.contains(r#"class="delete is-small""#),
"expected delete button when closable=true; got: {}",
html
);
}
}