presentar_core/
brick_widget.rs1use crate::widget::{Brick, BrickAssertion, BrickBudget, BrickVerification};
31use std::time::Duration;
32
33#[derive(Debug, Clone)]
38pub struct SimpleBrick {
39 name: &'static str,
40 assertions: Vec<BrickAssertion>,
41 budget: BrickBudget,
42 custom_verify: Option<fn() -> bool>,
43}
44
45impl SimpleBrick {
46 #[must_use]
48 pub const fn new(name: &'static str) -> Self {
49 Self {
50 name,
51 assertions: Vec::new(),
52 budget: BrickBudget::uniform(16), custom_verify: None,
54 }
55 }
56
57 #[must_use]
59 pub fn with_assertion(mut self, assertion: BrickAssertion) -> Self {
60 self.assertions.push(assertion);
61 self
62 }
63
64 #[must_use]
66 pub const fn with_budget(mut self, budget: BrickBudget) -> Self {
67 self.budget = budget;
68 self
69 }
70
71 #[must_use]
73 pub const fn with_custom_verify(mut self, verify: fn() -> bool) -> Self {
74 self.custom_verify = Some(verify);
75 self
76 }
77}
78
79impl Brick for SimpleBrick {
80 fn brick_name(&self) -> &'static str {
81 self.name
82 }
83
84 fn assertions(&self) -> &[BrickAssertion] {
85 &self.assertions
86 }
87
88 fn budget(&self) -> BrickBudget {
89 self.budget
90 }
91
92 fn verify(&self) -> BrickVerification {
93 let mut passed = Vec::new();
94 let mut failed = Vec::new();
95
96 if let Some(verify_fn) = self.custom_verify {
98 if !verify_fn() {
99 failed.push((
100 BrickAssertion::Custom {
101 name: "custom_verify".into(),
102 validator_id: 0,
103 },
104 "Custom verification failed".into(),
105 ));
106 }
107 }
108
109 for assertion in &self.assertions {
111 passed.push(assertion.clone());
112 }
113
114 BrickVerification {
115 passed,
116 failed,
117 verification_time: Duration::from_micros(1),
118 }
119 }
120
121 fn to_html(&self) -> String {
122 format!(r#"<div class="brick brick-{}">"#, self.name)
123 }
124
125 fn to_css(&self) -> String {
126 format!(".brick-{} {{ display: block; }}", self.name)
127 }
128}
129
130#[derive(Debug, Clone, Copy)]
135pub struct DefaultBrick;
136
137impl Brick for DefaultBrick {
138 fn brick_name(&self) -> &'static str {
139 "DefaultBrick"
140 }
141
142 fn assertions(&self) -> &[BrickAssertion] {
143 &[]
144 }
145
146 fn budget(&self) -> BrickBudget {
147 BrickBudget::uniform(16)
148 }
149
150 fn verify(&self) -> BrickVerification {
151 BrickVerification {
152 passed: vec![],
153 failed: vec![],
154 verification_time: Duration::from_micros(1),
155 }
156 }
157
158 fn to_html(&self) -> String {
159 String::new()
160 }
161
162 fn to_css(&self) -> String {
163 String::new()
164 }
165}
166
167pub trait BrickWidgetExt: Brick {
169 fn verify_for_render(&self) -> Result<(), String> {
173 if self.can_render() {
174 Ok(())
175 } else {
176 let verification = self.verify();
177 let errors: Vec<String> = verification
178 .failed
179 .iter()
180 .map(|(assertion, reason)| format!("{assertion:?}: {reason}"))
181 .collect();
182 Err(format!(
183 "Brick '{}' failed verification: {}",
184 self.brick_name(),
185 errors.join(", ")
186 ))
187 }
188 }
189}
190
191impl<T: Brick> BrickWidgetExt for T {}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_simple_brick_new() {
199 let brick = SimpleBrick::new("TestBrick");
200 assert_eq!(brick.brick_name(), "TestBrick");
201 assert!(brick.assertions().is_empty());
202 }
203
204 #[test]
205 fn test_simple_brick_with_assertion() {
206 let brick = SimpleBrick::new("TestBrick")
207 .with_assertion(BrickAssertion::TextVisible)
208 .with_assertion(BrickAssertion::ContrastRatio(4.5));
209
210 assert_eq!(brick.assertions().len(), 2);
211 }
212
213 #[test]
214 fn test_simple_brick_with_budget() {
215 let brick = SimpleBrick::new("TestBrick").with_budget(BrickBudget::uniform(32));
216
217 assert_eq!(brick.budget().total_ms, 32);
218 }
219
220 #[test]
221 fn test_simple_brick_verify() {
222 let brick = SimpleBrick::new("TestBrick");
223 let verification = brick.verify();
224 assert!(verification.is_valid());
225 }
226
227 #[test]
228 fn test_simple_brick_can_render() {
229 let brick = SimpleBrick::new("TestBrick");
230 assert!(brick.can_render());
231 }
232
233 #[test]
234 fn test_default_brick() {
235 let brick = DefaultBrick;
236 assert_eq!(brick.brick_name(), "DefaultBrick");
237 assert!(brick.can_render());
238 }
239
240 #[test]
241 fn test_verify_for_render() {
242 let brick = SimpleBrick::new("TestBrick");
243 assert!(brick.verify_for_render().is_ok());
244 }
245
246 #[test]
247 fn test_simple_brick_with_custom_verify_pass() {
248 let brick = SimpleBrick::new("TestBrick").with_custom_verify(|| true);
249 let verification = brick.verify();
250 assert!(verification.is_valid());
251 assert!(verification.failed.is_empty());
252 }
253
254 #[test]
255 fn test_simple_brick_with_custom_verify_fail() {
256 let brick = SimpleBrick::new("TestBrick").with_custom_verify(|| false);
257 let verification = brick.verify();
258 assert!(!verification.is_valid());
259 assert_eq!(verification.failed.len(), 1);
260 assert!(verification.failed[0]
261 .1
262 .contains("Custom verification failed"));
263 }
264
265 #[test]
266 fn test_simple_brick_to_html() {
267 let brick = SimpleBrick::new("MyWidget");
268 let html = brick.to_html();
269 assert!(html.contains("brick-MyWidget"));
270 assert!(html.starts_with("<div"));
271 }
272
273 #[test]
274 fn test_simple_brick_to_css() {
275 let brick = SimpleBrick::new("MyWidget");
276 let css = brick.to_css();
277 assert!(css.contains(".brick-MyWidget"));
278 assert!(css.contains("display: block"));
279 }
280
281 #[test]
282 fn test_default_brick_to_html() {
283 let brick = DefaultBrick;
284 assert!(brick.to_html().is_empty());
285 }
286
287 #[test]
288 fn test_default_brick_to_css() {
289 let brick = DefaultBrick;
290 assert!(brick.to_css().is_empty());
291 }
292
293 #[test]
294 fn test_default_brick_assertions_empty() {
295 let brick = DefaultBrick;
296 assert!(brick.assertions().is_empty());
297 }
298
299 #[test]
300 fn test_default_brick_budget() {
301 let brick = DefaultBrick;
302 assert_eq!(brick.budget().total_ms, 16);
303 }
304
305 #[test]
306 fn test_default_brick_verify() {
307 let brick = DefaultBrick;
308 let verification = brick.verify();
309 assert!(verification.passed.is_empty());
310 assert!(verification.failed.is_empty());
311 }
312
313 #[test]
314 fn test_verify_for_render_with_custom_fail() {
315 let brick = SimpleBrick::new("FailBrick").with_custom_verify(|| false);
316 let result = brick.verify_for_render();
317 assert!(result.is_err());
318 let err = result.unwrap_err();
319 assert!(err.contains("FailBrick"));
320 assert!(err.contains("failed verification"));
321 }
322
323 #[test]
324 fn test_simple_brick_clone() {
325 let brick = SimpleBrick::new("CloneTest")
326 .with_assertion(BrickAssertion::TextVisible)
327 .with_budget(BrickBudget::uniform(32));
328 let cloned = brick.clone();
329 assert_eq!(cloned.brick_name(), brick.brick_name());
330 assert_eq!(cloned.assertions().len(), brick.assertions().len());
331 }
332
333 #[test]
334 fn test_simple_brick_debug() {
335 let brick = SimpleBrick::new("DebugTest");
336 let debug = format!("{brick:?}");
337 assert!(debug.contains("SimpleBrick"));
338 assert!(debug.contains("DebugTest"));
339 }
340
341 #[test]
342 fn test_default_brick_copy() {
343 let brick = DefaultBrick;
344 let copied = brick;
345 assert_eq!(copied.brick_name(), brick.brick_name());
346 }
347}