presentar_core/
brick_widget.rs

1//! Brick-based Widget helpers (PROBAR-SPEC-009)
2//!
3//! This module provides helpers for creating Widgets that implement the Brick trait,
4//! enabling the "tests define interface" philosophy.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use presentar_core::brick_widget::{SimpleBrick, BrickWidgetExt};
10//! use jugar_probar::brick::{BrickAssertion, BrickBudget};
11//!
12//! struct MyWidget {
13//!     text: String,
14//!     brick: SimpleBrick,
15//! }
16//!
17//! impl MyWidget {
18//!     fn new(text: &str) -> Self {
19//!         Self {
20//!             text: text.to_string(),
21//!             brick: SimpleBrick::new("MyWidget")
22//!                 .with_assertion(BrickAssertion::TextVisible)
23//!                 .with_assertion(BrickAssertion::ContrastRatio(4.5))
24//!                 .with_budget(BrickBudget::uniform(16)),
25//!         }
26//!     }
27//! }
28//! ```
29
30use crate::widget::{Brick, BrickAssertion, BrickBudget, BrickVerification};
31use std::time::Duration;
32
33/// Simple Brick implementation for common use cases.
34///
35/// Provides a straightforward way to define brick assertions and budgets
36/// without implementing the full Brick trait manually.
37#[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    /// Create a new `SimpleBrick` with the given name.
47    #[must_use]
48    pub const fn new(name: &'static str) -> Self {
49        Self {
50            name,
51            assertions: Vec::new(),
52            budget: BrickBudget::uniform(16), // 60fps default
53            custom_verify: None,
54        }
55    }
56
57    /// Add an assertion to this brick.
58    #[must_use]
59    pub fn with_assertion(mut self, assertion: BrickAssertion) -> Self {
60        self.assertions.push(assertion);
61        self
62    }
63
64    /// Set the performance budget.
65    #[must_use]
66    pub const fn with_budget(mut self, budget: BrickBudget) -> Self {
67        self.budget = budget;
68        self
69    }
70
71    /// Add a custom verification function.
72    #[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        // Run custom verification if provided
97        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        // All assertions pass by default (actual verification happens at render time)
110        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/// Default Brick implementation for simple widgets.
131///
132/// Use this when you need a minimal Brick implementation
133/// that always passes verification.
134#[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
167/// Extension trait for adding Brick verification to the render pipeline.
168pub trait BrickWidgetExt: Brick {
169    /// Verify this brick before rendering.
170    ///
171    /// Returns an error if any assertion fails.
172    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}