1use leptos::prelude::{
2 Children, ClassAttribute, CustomAttribute, ElementChild, Get, IntoView, Signal, component, view,
3};
4
5use crate::util::TestAttr;
6
7fn base_class(root: &str, extra: &str) -> String {
8 if extra.trim().is_empty() {
9 root.to_string()
10 } else {
11 format!("{root} {extra}")
12 }
13}
14
15#[component]
18pub fn Menu(
19 #[prop(optional, into)]
21 classes: Signal<String>,
22
23 #[prop(optional, into)]
28 test_attr: Option<TestAttr>,
29
30 children: Children,
32) -> impl IntoView {
33 let class = {
34 let classes = classes.clone();
35 move || base_class("menu", &classes.get())
36 };
37
38 let (data_testid, data_cy) = match &test_attr {
39 Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
40 Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
41 _ => (None, None),
42 };
43
44 view! {
45 <aside
46 class=class
47 attr:data-testid=move || data_testid.clone()
48 attr:data-cy=move || data_cy.clone()
49 >
50 {children()}
51 </aside>
52 }
53}
54
55#[component]
58pub fn MenuList(
59 children: Children,
61
62 #[prop(optional, into)]
64 classes: Signal<String>,
65
66 #[prop(optional, into)]
71 test_attr: Option<TestAttr>,
72) -> impl IntoView {
73 let class = {
74 let classes = classes.clone();
75 move || base_class("menu-list", &classes.get())
76 };
77
78 let (data_testid, data_cy) = match &test_attr {
79 Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
80 Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
81 _ => (None, None),
82 };
83
84 view! {
85 <ul
86 class=class
87 attr:data-testid=move || data_testid.clone()
88 attr:data-cy=move || data_cy.clone()
89 >
90 {children()}
91 </ul>
92 }
93}
94
95#[component]
98pub fn MenuLabel(
99 #[prop(optional, into)]
101 classes: Signal<String>,
102
103 #[prop(optional, into)]
105 text: Signal<String>,
106
107 #[prop(optional, into)]
112 test_attr: Option<TestAttr>,
113) -> impl IntoView {
114 let class = {
115 let classes = classes.clone();
116 move || base_class("menu-label", &classes.get())
117 };
118
119 let (data_testid, data_cy) = match &test_attr {
120 Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
121 Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
122 _ => (None, None),
123 };
124
125 view! {
126 <p
127 class=class
128 attr:data-testid=move || data_testid.clone()
129 attr:data-cy=move || data_cy.clone()
130 >
131 {text.get()}
132 </p>
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use leptos::prelude::RenderHtml;
140
141 #[test]
142 fn menu_renders_base_class_and_children() {
143 let html = view! { <Menu><div>"X"</div></Menu> }.to_html();
144 assert!(
145 html.contains(r#"class="menu""#),
146 "expected base 'menu' class; got: {}",
147 html
148 );
149 assert!(
150 html.contains("X"),
151 "expected children rendered; got: {}",
152 html
153 );
154 }
155
156 #[test]
157 fn menu_list_renders_container() {
158 let html = view! { <MenuList><li><a>"Item"</a></li></MenuList> }.to_html();
159 assert!(
160 html.contains(r#"class="menu-list""#),
161 "expected 'menu-list' class; got: {}",
162 html
163 );
164 assert!(html.contains("Item"), "expected list child; got: {}", html);
165 }
166
167 #[test]
168 fn menu_label_renders_text() {
169 let html = view! { <MenuLabel text="General" /> }.to_html();
170 assert!(
171 html.contains(r#"class="menu-label""#),
172 "expected 'menu-label' class; got: {}",
173 html
174 );
175 assert!(
176 html.contains("General"),
177 "expected label text; got: {}",
178 html
179 );
180 }
181}
182
183#[cfg(all(test, target_arch = "wasm32"))]
184mod wasm_tests {
185 use super::*;
186 use crate::util::TestAttr;
187 use leptos::prelude::*;
188 use wasm_bindgen_test::*;
189
190 wasm_bindgen_test_configure!(run_in_browser);
191
192 #[wasm_bindgen_test]
193 fn menu_renders_test_attr_as_data_testid() {
194 let html = view! {
195 <Menu classes="extra" test_attr="menu-test">
196 <div>"X"</div>
197 </Menu>
198 }
199 .to_html();
200
201 assert!(
202 html.contains(r#"data-testid="menu-test""#),
203 "expected data-testid attribute on Menu; got: {}",
204 html
205 );
206 }
207
208 #[wasm_bindgen_test]
209 fn menu_no_test_attr_when_not_provided() {
210 let html = view! {
211 <Menu>
212 <div>"X"</div>
213 </Menu>
214 }
215 .to_html();
216
217 assert!(
218 !html.contains("data-testid") && !html.contains("data-cy"),
219 "expected no test attribute on Menu when not provided; got: {}",
220 html
221 );
222 }
223
224 #[wasm_bindgen_test]
225 fn menu_list_renders_test_attr_as_data_testid() {
226 let html = view! {
227 <MenuList classes="extra" test_attr="menu-list-test">
228 <li><a>"Item"</a></li>
229 </MenuList>
230 }
231 .to_html();
232
233 assert!(
234 html.contains(r#"data-testid="menu-list-test""#),
235 "expected data-testid attribute on MenuList; got: {}",
236 html
237 );
238 }
239
240 #[wasm_bindgen_test]
241 fn menu_label_renders_test_attr_as_data_testid() {
242 let html = view! {
243 <MenuLabel text="General" test_attr="menu-label-test" />
244 }
245 .to_html();
246
247 assert!(
248 html.contains(r#"data-testid="menu-label-test""#),
249 "expected data-testid attribute on MenuLabel; got: {}",
250 html
251 );
252 }
253
254 #[wasm_bindgen_test]
255 fn menu_accepts_custom_test_attr_key() {
256 let html = view! {
257 <Menu
258 classes="extra"
259 test_attr=TestAttr::new("data-cy", "menu-cy")
260 >
261 <div>"X"</div>
262 </Menu>
263 }
264 .to_html();
265
266 assert!(
267 html.contains(r#"data-cy="menu-cy""#),
268 "expected custom data-cy attribute on Menu; got: {}",
269 html
270 );
271 }
272}