Skip to main content

jugar_probar/brick/
web_sys_gen.rs

1//! WebSys Code Generation (PROBAR-SPEC-009-P7: PROBAR-WEBSYS-001)
2//!
3//! Generates web_sys binding code from brick definitions.
4//! This module provides abstractions that replace hand-written web_sys calls.
5//!
6//! # Design Philosophy
7//!
8//! Instead of hand-writing web_sys calls like:
9//! ```rust,ignore
10//! let start = web_sys::window().unwrap().performance().unwrap().now();
11//! ```
12//!
13//! Use generated abstractions:
14//! ```rust,ignore
15//! use probar::brick::web_sys_gen::Performance;
16//! let start = Performance::now();
17//! ```
18//!
19//! The generated code is still web_sys underneath, but:
20//! 1. It's derived from brick specifications (traceable)
21//! 2. Error handling is consistent
22//! 3. No hand-written web_sys in application code
23
24use std::fmt;
25
26// ============================================================================
27// Performance Timing (replaces web_sys::window().performance())
28// ============================================================================
29
30/// Generated performance timing utilities
31///
32/// Replaces hand-written:
33/// ```rust,ignore
34/// web_sys::window().unwrap().performance().unwrap().now()
35/// ```
36#[derive(Debug, Clone, Copy)]
37pub struct PerformanceTiming;
38
39impl PerformanceTiming {
40    /// Get current timestamp in milliseconds (high resolution)
41    ///
42    /// Generated binding for `performance.now()`
43    #[cfg(target_arch = "wasm32")]
44    #[must_use]
45    pub fn now() -> f64 {
46        web_sys::window()
47            .and_then(|w| w.performance())
48            .map(|p| p.now())
49            .unwrap_or(0.0)
50    }
51
52    /// Get current timestamp (native fallback)
53    #[cfg(not(target_arch = "wasm32"))]
54    #[must_use]
55    pub fn now() -> f64 {
56        use std::time::{SystemTime, UNIX_EPOCH};
57        SystemTime::now()
58            .duration_since(UNIX_EPOCH)
59            .map(|d| d.as_secs_f64() * 1000.0)
60            .unwrap_or(0.0)
61    }
62
63    /// Measure duration of an operation
64    #[must_use]
65    pub fn measure<F, T>(f: F) -> (T, f64)
66    where
67        F: FnOnce() -> T,
68    {
69        let start = Self::now();
70        let result = f();
71        let duration = Self::now() - start;
72        (result, duration)
73    }
74}
75
76// ============================================================================
77// Custom Events (replaces web_sys::CustomEvent)
78// ============================================================================
79
80/// Event detail that can be serialized to JS
81#[derive(Debug, Clone)]
82pub enum EventDetail {
83    /// No detail
84    None,
85    /// String detail
86    String(String),
87    /// Number detail
88    Number(f64),
89    /// Boolean detail
90    Bool(bool),
91    /// JSON object detail
92    Json(String),
93}
94
95impl EventDetail {
96    /// Create from a string
97    #[must_use]
98    pub fn string(s: impl Into<String>) -> Self {
99        Self::String(s.into())
100    }
101
102    /// Create from a number
103    #[must_use]
104    pub fn number(n: f64) -> Self {
105        Self::Number(n)
106    }
107
108    /// Create JSON detail from serializable value
109    #[must_use]
110    pub fn json<T: serde::Serialize>(value: &T) -> Self {
111        match serde_json::to_string(value) {
112            Ok(s) => Self::Json(s),
113            Err(_) => Self::None,
114        }
115    }
116}
117
118/// Generated custom event dispatcher
119///
120/// Replaces hand-written:
121/// ```rust,ignore
122/// let init = web_sys::CustomEventInit::new();
123/// init.set_detail(&detail.into());
124/// let event = web_sys::CustomEvent::new_with_event_init_dict("my-event", &init)?;
125/// window.dispatch_event(&event)?;
126/// ```
127#[derive(Debug, Clone)]
128pub struct CustomEventDispatcher {
129    #[allow(dead_code)] // Used only in wasm32 target
130    event_name: String,
131}
132
133impl CustomEventDispatcher {
134    /// Create a new event dispatcher for a specific event type
135    #[must_use]
136    pub fn new(event_name: impl Into<String>) -> Self {
137        Self {
138            event_name: event_name.into(),
139        }
140    }
141
142    /// Dispatch event with no detail
143    #[cfg(target_arch = "wasm32")]
144    pub fn dispatch(&self) -> Result<bool, WebSysError> {
145        use wasm_bindgen::JsCast;
146
147        let window = web_sys::window().ok_or(WebSysError::NoWindow)?;
148
149        let event = web_sys::CustomEvent::new(&self.event_name)
150            .map_err(|_| WebSysError::EventCreationFailed)?;
151
152        window
153            .dispatch_event(&event)
154            .map_err(|_| WebSysError::DispatchFailed)
155    }
156
157    /// Dispatch event with detail
158    #[cfg(target_arch = "wasm32")]
159    pub fn dispatch_with_detail(&self, detail: EventDetail) -> Result<bool, WebSysError> {
160        use wasm_bindgen::JsValue;
161
162        let window = web_sys::window().ok_or(WebSysError::NoWindow)?;
163
164        let init = web_sys::CustomEventInit::new();
165
166        let js_detail: JsValue = match detail {
167            EventDetail::None => JsValue::NULL,
168            EventDetail::String(s) => JsValue::from_str(&s),
169            EventDetail::Number(n) => JsValue::from_f64(n),
170            EventDetail::Bool(b) => JsValue::from_bool(b),
171            EventDetail::Json(json) => js_sys::JSON::parse(&json).unwrap_or(JsValue::NULL),
172        };
173
174        init.set_detail(&js_detail);
175
176        let event = web_sys::CustomEvent::new_with_event_init_dict(&self.event_name, &init)
177            .map_err(|_| WebSysError::EventCreationFailed)?;
178
179        window
180            .dispatch_event(&event)
181            .map_err(|_| WebSysError::DispatchFailed)
182    }
183
184    /// Native fallback - no-op
185    #[cfg(not(target_arch = "wasm32"))]
186    pub fn dispatch(&self) -> Result<bool, WebSysError> {
187        Ok(true)
188    }
189
190    /// Native fallback - no-op
191    #[cfg(not(target_arch = "wasm32"))]
192    pub fn dispatch_with_detail(&self, _detail: EventDetail) -> Result<bool, WebSysError> {
193        Ok(true)
194    }
195}
196
197// ============================================================================
198// Fetch API (replaces web_sys::window().fetch_with_str())
199// ============================================================================
200
201/// Generated fetch result
202#[derive(Debug)]
203pub struct FetchResult {
204    /// Response status code
205    pub status: u16,
206    /// Response body as bytes
207    pub body: Vec<u8>,
208}
209
210/// Generated fetch client
211///
212/// Replaces hand-written fetch calls
213#[derive(Debug, Clone, Default)]
214pub struct FetchClient;
215
216impl FetchClient {
217    /// Create a new fetch client
218    #[must_use]
219    pub fn new() -> Self {
220        Self
221    }
222
223    /// Fetch bytes from a URL (WASM)
224    /// Works in both main thread (window) and Web Worker (self) contexts
225    #[cfg(target_arch = "wasm32")]
226    pub async fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, WebSysError> {
227        use wasm_bindgen::JsCast;
228        use wasm_bindgen_futures::JsFuture;
229
230        // Use global fetch which works in both Window and Worker contexts
231        let global = js_sys::global();
232        let fetch_fn = js_sys::Reflect::get(&global, &wasm_bindgen::JsValue::from_str("fetch"))
233            .map_err(|_| WebSysError::NoWindow)?;
234        let fetch_fn: js_sys::Function = fetch_fn.dyn_into().map_err(|_| WebSysError::NoWindow)?;
235
236        let promise = fetch_fn
237            .call1(
238                &wasm_bindgen::JsValue::UNDEFINED,
239                &wasm_bindgen::JsValue::from_str(url),
240            )
241            .map_err(|_| WebSysError::FetchFailed)?;
242
243        let response = JsFuture::from(js_sys::Promise::from(promise))
244            .await
245            .map_err(|_| WebSysError::FetchFailed)?;
246
247        let response: web_sys::Response =
248            response.dyn_into().map_err(|_| WebSysError::FetchFailed)?;
249
250        let array_buffer = JsFuture::from(
251            response
252                .array_buffer()
253                .map_err(|_| WebSysError::FetchFailed)?,
254        )
255        .await
256        .map_err(|_| WebSysError::FetchFailed)?;
257
258        let uint8_array = js_sys::Uint8Array::new(&array_buffer);
259        Ok(uint8_array.to_vec())
260    }
261
262    /// Fetch bytes from a URL (native fallback - returns error)
263    #[cfg(not(target_arch = "wasm32"))]
264    #[allow(clippy::unused_async)] // Must be async for API compatibility with WASM target
265    pub async fn fetch_bytes(&self, _url: &str) -> Result<Vec<u8>, WebSysError> {
266        Err(WebSysError::NotInBrowser)
267    }
268}
269
270// ============================================================================
271// Blob URL Generation (replaces web_sys::Blob, web_sys::Url)
272// ============================================================================
273
274/// Generated blob URL creator
275///
276/// Replaces hand-written:
277/// ```rust,ignore
278/// let options = web_sys::BlobPropertyBag::new();
279/// options.set_type("application/javascript");
280/// let blob = web_sys::Blob::new_with_blob_sequence_and_options(&parts, &options)?;
281/// web_sys::Url::create_object_url_with_blob(&blob)?
282/// ```
283#[derive(Debug, Clone)]
284pub struct BlobUrl;
285
286impl BlobUrl {
287    /// Create a blob URL from JavaScript code
288    #[cfg(target_arch = "wasm32")]
289    pub fn from_js_code(code: &str) -> Result<String, WebSysError> {
290        use wasm_bindgen::JsValue;
291
292        let options = web_sys::BlobPropertyBag::new();
293        options.set_type("application/javascript");
294
295        let js_string = JsValue::from_str(code);
296        let blob_parts = js_sys::Array::new();
297        blob_parts.push(&js_string);
298
299        let blob = web_sys::Blob::new_with_blob_sequence_and_options(&blob_parts, &options)
300            .map_err(|_| WebSysError::BlobCreationFailed)?;
301
302        web_sys::Url::create_object_url_with_blob(&blob).map_err(|_| WebSysError::UrlCreationFailed)
303    }
304
305    /// Revoke a blob URL
306    #[cfg(target_arch = "wasm32")]
307    pub fn revoke(url: &str) -> Result<(), WebSysError> {
308        web_sys::Url::revoke_object_url(url).map_err(|_| WebSysError::UrlRevokeFailed)
309    }
310
311    /// Native fallback
312    #[cfg(not(target_arch = "wasm32"))]
313    pub fn from_js_code(_code: &str) -> Result<String, WebSysError> {
314        Err(WebSysError::NotInBrowser)
315    }
316
317    /// Native fallback
318    #[cfg(not(target_arch = "wasm32"))]
319    pub fn revoke(_url: &str) -> Result<(), WebSysError> {
320        Ok(())
321    }
322}
323
324// ============================================================================
325// Base URL (replaces web_sys::window().location().href())
326// ============================================================================
327
328/// Get the base URL of the current page
329#[cfg(target_arch = "wasm32")]
330#[must_use]
331pub fn get_base_url() -> Option<String> {
332    web_sys::window()
333        .and_then(|w| w.location().href().ok())
334        .and_then(|href| {
335            // Strip filename to get directory
336            href.rsplit_once('/').map(|(base, _)| format!("{}/", base))
337        })
338}
339
340/// Native fallback
341#[cfg(not(target_arch = "wasm32"))]
342#[must_use]
343pub fn get_base_url() -> Option<String> {
344    Some("http://localhost/".to_string())
345}
346
347// ============================================================================
348// Web Worker Creation (replaces web_sys::Worker)
349// ============================================================================
350
351/// Generated web worker handle
352#[cfg(target_arch = "wasm32")]
353pub struct GeneratedWorker {
354    inner: web_sys::Worker,
355    _on_message: wasm_bindgen::closure::Closure<dyn Fn(web_sys::MessageEvent)>,
356}
357
358#[cfg(target_arch = "wasm32")]
359impl GeneratedWorker {
360    /// Create a new worker from JavaScript code
361    pub fn from_code<F>(code: &str, on_message: F) -> Result<Self, WebSysError>
362    where
363        F: Fn(web_sys::MessageEvent) + 'static,
364    {
365        use wasm_bindgen::closure::Closure;
366        use wasm_bindgen::JsCast;
367
368        let worker_url = BlobUrl::from_js_code(code)?;
369
370        let worker_options = web_sys::WorkerOptions::new();
371        worker_options.set_type(web_sys::WorkerType::Module);
372
373        let worker = web_sys::Worker::new_with_options(&worker_url, &worker_options)
374            .map_err(|_| WebSysError::WorkerCreationFailed)?;
375
376        // Revoke the blob URL after worker is created
377        let _ = BlobUrl::revoke(&worker_url);
378
379        // Set up message handler
380        let on_message_closure =
381            Closure::wrap(Box::new(on_message) as Box<dyn Fn(web_sys::MessageEvent)>);
382
383        worker.set_onmessage(Some(on_message_closure.as_ref().unchecked_ref()));
384
385        Ok(Self {
386            inner: worker,
387            _on_message: on_message_closure,
388        })
389    }
390
391    /// Post a message to the worker
392    pub fn post_message(&self, message: &wasm_bindgen::JsValue) -> Result<(), WebSysError> {
393        self.inner
394            .post_message(message)
395            .map_err(|_| WebSysError::PostMessageFailed)
396    }
397
398    /// Terminate the worker
399    pub fn terminate(&self) {
400        self.inner.terminate();
401    }
402}
403
404#[cfg(target_arch = "wasm32")]
405impl fmt::Debug for GeneratedWorker {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        f.debug_struct("GeneratedWorker").finish()
408    }
409}
410
411// ============================================================================
412// Error Types
413// ============================================================================
414
415/// Errors from web_sys operations
416#[derive(Debug, Clone)]
417pub enum WebSysError {
418    /// No window object available
419    NoWindow,
420    /// Event creation failed
421    EventCreationFailed,
422    /// Event dispatch failed
423    DispatchFailed,
424    /// Fetch operation failed
425    FetchFailed,
426    /// Not running in browser
427    NotInBrowser,
428    /// Blob creation failed
429    BlobCreationFailed,
430    /// URL creation failed
431    UrlCreationFailed,
432    /// URL revoke failed
433    UrlRevokeFailed,
434    /// Worker creation failed
435    WorkerCreationFailed,
436    /// Post message failed
437    PostMessageFailed,
438}
439
440impl fmt::Display for WebSysError {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        match self {
443            Self::NoWindow => write!(f, "No window object available"),
444            Self::EventCreationFailed => write!(f, "Failed to create custom event"),
445            Self::DispatchFailed => write!(f, "Failed to dispatch event"),
446            Self::FetchFailed => write!(f, "Fetch operation failed"),
447            Self::NotInBrowser => write!(f, "Not running in browser environment"),
448            Self::BlobCreationFailed => write!(f, "Failed to create blob"),
449            Self::UrlCreationFailed => write!(f, "Failed to create object URL"),
450            Self::UrlRevokeFailed => write!(f, "Failed to revoke object URL"),
451            Self::WorkerCreationFailed => write!(f, "Failed to create web worker"),
452            Self::PostMessageFailed => write!(f, "Failed to post message to worker"),
453        }
454    }
455}
456
457impl std::error::Error for WebSysError {}
458
459// ============================================================================
460// Code Generation Metadata
461// ============================================================================
462
463/// Marker trait for generated web_sys code
464///
465/// All generated web_sys code implements this trait for traceability
466pub trait GeneratedWebSys {
467    /// Source brick that generated this code
468    fn source_brick() -> &'static str;
469
470    /// Generation timestamp
471    fn generated_at() -> &'static str;
472}
473
474/// Metadata about generated code
475#[derive(Debug, Clone)]
476pub struct GenerationMetadata {
477    /// Source specification
478    pub spec: &'static str,
479    /// Ticket reference
480    pub ticket: &'static str,
481    /// Generation method
482    pub method: &'static str,
483}
484
485/// Standard generation metadata for this module
486pub const GENERATION_METADATA: GenerationMetadata = GenerationMetadata {
487    spec: "PROBAR-SPEC-009-P7",
488    ticket: "PROBAR-WEBSYS-001",
489    method: "probar::brick::web_sys_gen",
490};
491
492// ============================================================================
493// Tests
494// ============================================================================
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn test_performance_timing_native() {
502        let t1 = PerformanceTiming::now();
503        std::thread::sleep(std::time::Duration::from_millis(10));
504        let t2 = PerformanceTiming::now();
505
506        assert!(t2 > t1);
507    }
508
509    #[test]
510    fn test_performance_measure() {
511        let (result, duration) = PerformanceTiming::measure(|| {
512            std::thread::sleep(std::time::Duration::from_millis(5));
513            42
514        });
515
516        assert_eq!(result, 42);
517        assert!(duration >= 4.0); // Allow some slack
518    }
519
520    #[test]
521    fn test_custom_event_dispatcher() {
522        let dispatcher = CustomEventDispatcher::new("test-event");
523        // Native fallback returns Ok
524        assert!(dispatcher.dispatch().is_ok());
525    }
526
527    #[test]
528    fn test_event_detail_variants() {
529        let string = EventDetail::string("hello");
530        assert!(matches!(string, EventDetail::String(_)));
531
532        let number = EventDetail::number(42.0);
533        assert!(matches!(number, EventDetail::Number(_)));
534
535        let json = EventDetail::json(&vec![1, 2, 3]);
536        assert!(matches!(json, EventDetail::Json(_)));
537    }
538
539    #[test]
540    fn test_fetch_client_native_fallback() {
541        let client = FetchClient::new();
542        // Can't actually test async in sync test, but verify it compiles
543        let _ = client;
544    }
545
546    #[test]
547    fn test_blob_url_native_fallback() {
548        let result = BlobUrl::from_js_code("console.log('test')");
549        assert!(matches!(result, Err(WebSysError::NotInBrowser)));
550
551        // Revoke should succeed (no-op)
552        assert!(BlobUrl::revoke("blob:test").is_ok());
553    }
554
555    #[test]
556    fn test_get_base_url_native() {
557        let url = get_base_url();
558        assert!(url.is_some());
559        assert!(url.unwrap().starts_with("http"));
560    }
561
562    #[test]
563    fn test_web_sys_error_display() {
564        let err = WebSysError::NoWindow;
565        assert_eq!(format!("{}", err), "No window object available");
566    }
567
568    #[test]
569    fn test_generation_metadata() {
570        assert_eq!(GENERATION_METADATA.spec, "PROBAR-SPEC-009-P7");
571        assert_eq!(GENERATION_METADATA.ticket, "PROBAR-WEBSYS-001");
572    }
573
574    // ========================================================================
575    // Additional tests for 95%+ coverage
576    // ========================================================================
577
578    #[test]
579    fn test_web_sys_error_all_variants_display() {
580        // Test all WebSysError variants for Display implementation
581        let errors = [
582            (WebSysError::NoWindow, "No window object available"),
583            (
584                WebSysError::EventCreationFailed,
585                "Failed to create custom event",
586            ),
587            (WebSysError::DispatchFailed, "Failed to dispatch event"),
588            (WebSysError::FetchFailed, "Fetch operation failed"),
589            (
590                WebSysError::NotInBrowser,
591                "Not running in browser environment",
592            ),
593            (WebSysError::BlobCreationFailed, "Failed to create blob"),
594            (
595                WebSysError::UrlCreationFailed,
596                "Failed to create object URL",
597            ),
598            (WebSysError::UrlRevokeFailed, "Failed to revoke object URL"),
599            (
600                WebSysError::WorkerCreationFailed,
601                "Failed to create web worker",
602            ),
603            (
604                WebSysError::PostMessageFailed,
605                "Failed to post message to worker",
606            ),
607        ];
608
609        for (error, expected_msg) in errors {
610            assert_eq!(format!("{}", error), expected_msg);
611        }
612    }
613
614    #[test]
615    fn test_web_sys_error_debug() {
616        let err = WebSysError::NoWindow;
617        let debug_str = format!("{:?}", err);
618        assert!(debug_str.contains("NoWindow"));
619    }
620
621    #[test]
622    fn test_web_sys_error_clone() {
623        let err = WebSysError::FetchFailed;
624        let cloned = err;
625        assert!(matches!(cloned, WebSysError::FetchFailed));
626    }
627
628    #[test]
629    fn test_web_sys_error_std_error_trait() {
630        let err: Box<dyn std::error::Error> = Box::new(WebSysError::NoWindow);
631        // Verify it can be used as a trait object
632        let _ = err.to_string();
633    }
634
635    #[test]
636    fn test_dispatch_with_detail_native_fallback() {
637        let dispatcher = CustomEventDispatcher::new("test-event");
638
639        // Test all EventDetail variants through dispatch_with_detail
640        assert!(dispatcher.dispatch_with_detail(EventDetail::None).is_ok());
641        assert!(dispatcher
642            .dispatch_with_detail(EventDetail::String("hello".to_string()))
643            .is_ok());
644        assert!(dispatcher
645            .dispatch_with_detail(EventDetail::Number(42.0))
646            .is_ok());
647        assert!(dispatcher
648            .dispatch_with_detail(EventDetail::Bool(true))
649            .is_ok());
650        assert!(dispatcher
651            .dispatch_with_detail(EventDetail::Json(r#"{"key":"value"}"#.to_string()))
652            .is_ok());
653    }
654
655    #[test]
656    fn test_event_detail_none() {
657        let detail = EventDetail::None;
658        assert!(matches!(detail, EventDetail::None));
659    }
660
661    #[test]
662    fn test_event_detail_bool() {
663        let detail_true = EventDetail::Bool(true);
664        let detail_false = EventDetail::Bool(false);
665        assert!(matches!(detail_true, EventDetail::Bool(true)));
666        assert!(matches!(detail_false, EventDetail::Bool(false)));
667    }
668
669    #[test]
670    fn test_event_detail_debug() {
671        let detail = EventDetail::String("test".to_string());
672        let debug_str = format!("{:?}", detail);
673        assert!(debug_str.contains("String"));
674    }
675
676    #[test]
677    fn test_event_detail_clone() {
678        let detail = EventDetail::Number(42.0);
679        let cloned = detail;
680        assert!(matches!(cloned, EventDetail::Number(n) if (n - 42.0).abs() < f64::EPSILON));
681    }
682
683    #[test]
684    fn test_event_detail_json_serialization_failure() {
685        // Test the error path when serialization fails
686        // Create a type that always fails to serialize
687        struct FailsToSerialize;
688
689        impl serde::Serialize for FailsToSerialize {
690            fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
691            where
692                S: serde::Serializer,
693            {
694                Err(serde::ser::Error::custom("intentional failure"))
695            }
696        }
697
698        let json = EventDetail::json(&FailsToSerialize);
699        // Should return None when serialization fails
700        assert!(matches!(json, EventDetail::None));
701
702        // Also verify the happy path still works
703        let json_ok = EventDetail::json(&"simple string");
704        assert!(matches!(json_ok, EventDetail::Json(_)));
705    }
706
707    #[tokio::test]
708    async fn test_fetch_bytes_native_fallback() {
709        // Test the async fetch_bytes method in native mode
710        let client = FetchClient::new();
711
712        let result = client.fetch_bytes("http://example.com").await;
713
714        // Native fallback should return NotInBrowser error
715        assert!(matches!(result, Err(WebSysError::NotInBrowser)));
716    }
717
718    #[test]
719    fn test_fetch_client_default() {
720        let client = FetchClient;
721        let debug_str = format!("{:?}", client);
722        assert!(debug_str.contains("FetchClient"));
723    }
724
725    #[test]
726    fn test_fetch_client_clone() {
727        let client = FetchClient::new();
728        let cloned = client;
729        let _ = format!("{:?}", cloned);
730    }
731
732    #[test]
733    fn test_fetch_result_struct() {
734        let result = FetchResult {
735            status: 200,
736            body: vec![1, 2, 3, 4],
737        };
738        assert_eq!(result.status, 200);
739        assert_eq!(result.body.len(), 4);
740
741        // Test Debug
742        let debug_str = format!("{:?}", result);
743        assert!(debug_str.contains("FetchResult"));
744        assert!(debug_str.contains("200"));
745    }
746
747    #[test]
748    fn test_custom_event_dispatcher_debug() {
749        let dispatcher = CustomEventDispatcher::new("my-event");
750        let debug_str = format!("{:?}", dispatcher);
751        assert!(debug_str.contains("CustomEventDispatcher"));
752    }
753
754    #[test]
755    fn test_custom_event_dispatcher_clone() {
756        let dispatcher = CustomEventDispatcher::new("clone-test");
757        let cloned = dispatcher;
758        assert!(cloned.dispatch().is_ok());
759    }
760
761    #[test]
762    fn test_performance_timing_debug() {
763        let timing = PerformanceTiming;
764        let debug_str = format!("{:?}", timing);
765        assert!(debug_str.contains("PerformanceTiming"));
766    }
767
768    #[test]
769    fn test_performance_timing_clone_copy() {
770        let timing1 = PerformanceTiming;
771        let timing2 = timing1; // Copy
772        let timing3 = timing1; // Clone
773        let _ = format!("{:?}", timing2);
774        let _ = format!("{:?}", timing3);
775    }
776
777    #[test]
778    fn test_blob_url_debug() {
779        let blob = BlobUrl;
780        let debug_str = format!("{:?}", blob);
781        assert!(debug_str.contains("BlobUrl"));
782    }
783
784    #[test]
785    fn test_blob_url_clone() {
786        let blob1 = BlobUrl;
787        let blob2 = blob1;
788        let _ = format!("{:?}", blob2);
789    }
790
791    #[test]
792    fn test_generation_metadata_debug() {
793        let debug_str = format!("{:?}", GENERATION_METADATA);
794        assert!(debug_str.contains("GenerationMetadata"));
795        assert!(debug_str.contains("PROBAR-SPEC-009-P7"));
796    }
797
798    #[test]
799    fn test_generation_metadata_clone() {
800        let cloned = GENERATION_METADATA.clone();
801        assert_eq!(cloned.spec, GENERATION_METADATA.spec);
802        assert_eq!(cloned.ticket, GENERATION_METADATA.ticket);
803        assert_eq!(cloned.method, GENERATION_METADATA.method);
804    }
805
806    #[test]
807    fn test_generation_metadata_method_field() {
808        assert_eq!(GENERATION_METADATA.method, "probar::brick::web_sys_gen");
809    }
810
811    #[test]
812    fn test_get_base_url_native_format() {
813        let url = get_base_url().expect("should return Some in native mode");
814        assert_eq!(url, "http://localhost/");
815    }
816
817    #[test]
818    fn test_performance_timing_now_returns_positive() {
819        let now = PerformanceTiming::now();
820        assert!(now > 0.0, "Timestamp should be positive");
821    }
822
823    #[test]
824    fn test_performance_timing_monotonic() {
825        let t1 = PerformanceTiming::now();
826        let t2 = PerformanceTiming::now();
827        let t3 = PerformanceTiming::now();
828        assert!(t2 >= t1);
829        assert!(t3 >= t2);
830    }
831
832    #[test]
833    fn test_event_detail_string_with_into() {
834        // Test that Into<String> works with various types
835        let s1 = EventDetail::string("literal str");
836        let s2 = EventDetail::string(String::from("String type"));
837
838        match s1 {
839            EventDetail::String(s) => assert_eq!(s, "literal str"),
840            _ => panic!("Expected String variant"),
841        }
842
843        match s2 {
844            EventDetail::String(s) => assert_eq!(s, "String type"),
845            _ => panic!("Expected String variant"),
846        }
847    }
848
849    #[test]
850    fn test_event_detail_number_special_values() {
851        // Test special floating point values
852        let nan = EventDetail::number(f64::NAN);
853        let inf = EventDetail::number(f64::INFINITY);
854        let neg_inf = EventDetail::number(f64::NEG_INFINITY);
855        let zero = EventDetail::number(0.0);
856        let neg_zero = EventDetail::number(-0.0);
857
858        assert!(matches!(nan, EventDetail::Number(_)));
859        assert!(matches!(inf, EventDetail::Number(_)));
860        assert!(matches!(neg_inf, EventDetail::Number(_)));
861        assert!(matches!(zero, EventDetail::Number(_)));
862        assert!(matches!(neg_zero, EventDetail::Number(_)));
863    }
864
865    #[test]
866    fn test_event_detail_json_complex_structures() {
867        use std::collections::HashMap;
868
869        // Test with nested HashMap
870        let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
871        map.insert("numbers", vec![1, 2, 3]);
872
873        let json = EventDetail::json(&map);
874        match json {
875            EventDetail::Json(s) => {
876                assert!(s.contains("numbers"));
877                assert!(s.contains("[1,2,3]"));
878            }
879            _ => panic!("Expected Json variant"),
880        }
881    }
882
883    #[test]
884    fn test_custom_event_dispatcher_new_with_string() {
885        let dispatcher1 = CustomEventDispatcher::new("event-name");
886        let dispatcher2 = CustomEventDispatcher::new(String::from("event-name-string"));
887        assert!(dispatcher1.dispatch().is_ok());
888        assert!(dispatcher2.dispatch().is_ok());
889    }
890
891    #[test]
892    fn test_blob_url_revoke_empty_string() {
893        // Revoke with empty string should still succeed in native fallback
894        assert!(BlobUrl::revoke("").is_ok());
895    }
896
897    #[test]
898    fn test_blob_url_from_js_code_empty() {
899        // Empty JS code should still return NotInBrowser in native
900        let result = BlobUrl::from_js_code("");
901        assert!(matches!(result, Err(WebSysError::NotInBrowser)));
902    }
903
904    #[test]
905    fn test_performance_measure_with_panic_safe() {
906        // Test measure with a quick operation
907        let (result, duration) = PerformanceTiming::measure(|| {
908            let mut sum = 0u64;
909            for i in 0..1000 {
910                sum = sum.wrapping_add(i);
911            }
912            sum
913        });
914
915        assert!(result > 0);
916        assert!(duration >= 0.0);
917    }
918
919    #[test]
920    fn test_fetch_result_empty_body() {
921        let result = FetchResult {
922            status: 204,
923            body: vec![],
924        };
925        assert_eq!(result.status, 204);
926        assert!(result.body.is_empty());
927    }
928
929    #[test]
930    fn test_fetch_result_large_body() {
931        let result = FetchResult {
932            status: 200,
933            body: vec![0u8; 10000],
934        };
935        assert_eq!(result.body.len(), 10000);
936    }
937
938    #[test]
939    fn test_web_sys_error_is_send_sync() {
940        fn assert_send<T: Send>() {}
941        fn assert_sync<T: Sync>() {}
942
943        assert_send::<WebSysError>();
944        assert_sync::<WebSysError>();
945    }
946
947    /// Test implementing GeneratedWebSys trait
948    struct TestGeneratedCode;
949
950    impl GeneratedWebSys for TestGeneratedCode {
951        fn source_brick() -> &'static str {
952            "test-brick"
953        }
954
955        fn generated_at() -> &'static str {
956            "2024-01-01T00:00:00Z"
957        }
958    }
959
960    #[test]
961    fn test_generated_websys_trait() {
962        assert_eq!(TestGeneratedCode::source_brick(), "test-brick");
963        assert_eq!(TestGeneratedCode::generated_at(), "2024-01-01T00:00:00Z");
964    }
965
966    #[test]
967    fn test_all_event_detail_variants_in_match() {
968        let variants = vec![
969            EventDetail::None,
970            EventDetail::String("test".to_string()),
971            EventDetail::Number(1.5),
972            EventDetail::Bool(false),
973            EventDetail::Json("{}".to_string()),
974        ];
975
976        for variant in variants {
977            let _ = match &variant {
978                EventDetail::None => "none",
979                EventDetail::String(_) => "string",
980                EventDetail::Number(_) => "number",
981                EventDetail::Bool(_) => "bool",
982                EventDetail::Json(_) => "json",
983            };
984        }
985    }
986}