Skip to main content

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::brick_types::{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)]
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}