presentar_core/
brick_widget.rs1use crate::brick_types::{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)]
194#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_simple_brick_new() {
200 let brick = SimpleBrick::new("TestBrick");
201 assert_eq!(brick.brick_name(), "TestBrick");
202 assert!(brick.assertions().is_empty());
203 }
204
205 #[test]
206 fn test_simple_brick_with_assertion() {
207 let brick = SimpleBrick::new("TestBrick")
208 .with_assertion(BrickAssertion::TextVisible)
209 .with_assertion(BrickAssertion::ContrastRatio(4.5));
210
211 assert_eq!(brick.assertions().len(), 2);
212 }
213
214 #[test]
215 fn test_simple_brick_with_budget() {
216 let brick = SimpleBrick::new("TestBrick").with_budget(BrickBudget::uniform(32));
217
218 assert_eq!(brick.budget().total_ms, 32);
219 }
220
221 #[test]
222 fn test_simple_brick_verify() {
223 let brick = SimpleBrick::new("TestBrick");
224 let verification = brick.verify();
225 assert!(verification.is_valid());
226 }
227
228 #[test]
229 fn test_simple_brick_can_render() {
230 let brick = SimpleBrick::new("TestBrick");
231 assert!(brick.can_render());
232 }
233
234 #[test]
235 fn test_default_brick() {
236 let brick = DefaultBrick;
237 assert_eq!(brick.brick_name(), "DefaultBrick");
238 assert!(brick.can_render());
239 }
240
241 #[test]
242 fn test_verify_for_render() {
243 let brick = SimpleBrick::new("TestBrick");
244 assert!(brick.verify_for_render().is_ok());
245 }
246
247 #[test]
248 fn test_simple_brick_with_custom_verify_pass() {
249 let brick = SimpleBrick::new("TestBrick").with_custom_verify(|| true);
250 let verification = brick.verify();
251 assert!(verification.is_valid());
252 assert!(verification.failed.is_empty());
253 }
254
255 #[test]
256 fn test_simple_brick_with_custom_verify_fail() {
257 let brick = SimpleBrick::new("TestBrick").with_custom_verify(|| false);
258 let verification = brick.verify();
259 assert!(!verification.is_valid());
260 assert_eq!(verification.failed.len(), 1);
261 assert!(verification.failed[0]
262 .1
263 .contains("Custom verification failed"));
264 }
265
266 #[test]
267 fn test_simple_brick_to_html() {
268 let brick = SimpleBrick::new("MyWidget");
269 let html = brick.to_html();
270 assert!(html.contains("brick-MyWidget"));
271 assert!(html.starts_with("<div"));
272 }
273
274 #[test]
275 fn test_simple_brick_to_css() {
276 let brick = SimpleBrick::new("MyWidget");
277 let css = brick.to_css();
278 assert!(css.contains(".brick-MyWidget"));
279 assert!(css.contains("display: block"));
280 }
281
282 #[test]
283 fn test_default_brick_to_html() {
284 let brick = DefaultBrick;
285 assert!(brick.to_html().is_empty());
286 }
287
288 #[test]
289 fn test_default_brick_to_css() {
290 let brick = DefaultBrick;
291 assert!(brick.to_css().is_empty());
292 }
293
294 #[test]
295 fn test_default_brick_assertions_empty() {
296 let brick = DefaultBrick;
297 assert!(brick.assertions().is_empty());
298 }
299
300 #[test]
301 fn test_default_brick_budget() {
302 let brick = DefaultBrick;
303 assert_eq!(brick.budget().total_ms, 16);
304 }
305
306 #[test]
307 fn test_default_brick_verify() {
308 let brick = DefaultBrick;
309 let verification = brick.verify();
310 assert!(verification.passed.is_empty());
311 assert!(verification.failed.is_empty());
312 }
313
314 #[test]
315 fn test_verify_for_render_with_custom_fail() {
316 let brick = SimpleBrick::new("FailBrick").with_custom_verify(|| false);
317 let result = brick.verify_for_render();
318 assert!(result.is_err());
319 let err = result.unwrap_err();
320 assert!(err.contains("FailBrick"));
321 assert!(err.contains("failed verification"));
322 }
323
324 #[test]
325 fn test_simple_brick_clone() {
326 let brick = SimpleBrick::new("CloneTest")
327 .with_assertion(BrickAssertion::TextVisible)
328 .with_budget(BrickBudget::uniform(32));
329 let cloned = brick.clone();
330 assert_eq!(cloned.brick_name(), brick.brick_name());
331 assert_eq!(cloned.assertions().len(), brick.assertions().len());
332 }
333
334 #[test]
335 fn test_simple_brick_debug() {
336 let brick = SimpleBrick::new("DebugTest");
337 let debug = format!("{brick:?}");
338 assert!(debug.contains("SimpleBrick"));
339 assert!(debug.contains("DebugTest"));
340 }
341
342 #[test]
343 fn test_default_brick_copy() {
344 let brick = DefaultBrick;
345 let copied = brick;
346 assert_eq!(copied.brick_name(), brick.brick_name());
347 }
348}