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    // Same reason as the `unused_async` allow that has been here since this
265    // fallback was written: the wasm32 sibling of this method IS async, and the
266    // two must have the same signature. clippy::unused_async_trait_impl is new
267    // in 1.98 and fires here despite `FetchClient` being an inherent impl, not
268    // a trait impl.
269    #[allow(unknown_lints, clippy::unused_async, clippy::unused_async_trait_impl)] // Must be async for API compatibility with WASM target
270    pub async fn fetch_bytes(&self, _url: &str) -> Result<Vec<u8>, WebSysError> {
271        Err(WebSysError::NotInBrowser)
272    }
273}
274
275// ============================================================================
276// Blob URL Generation (replaces web_sys::Blob, web_sys::Url)
277// ============================================================================
278
279/// Generated blob URL creator
280///
281/// Replaces hand-written:
282/// ```rust,ignore
283/// let options = web_sys::BlobPropertyBag::new();
284/// options.set_type("application/javascript");
285/// let blob = web_sys::Blob::new_with_blob_sequence_and_options(&parts, &options)?;
286/// web_sys::Url::create_object_url_with_blob(&blob)?
287/// ```
288#[derive(Debug, Clone)]
289pub struct BlobUrl;
290
291impl BlobUrl {
292    /// Create a blob URL from JavaScript code
293    #[cfg(target_arch = "wasm32")]
294    pub fn from_js_code(code: &str) -> Result<String, WebSysError> {
295        use wasm_bindgen::JsValue;
296
297        let options = web_sys::BlobPropertyBag::new();
298        options.set_type("application/javascript");
299
300        let js_string = JsValue::from_str(code);
301        let blob_parts = js_sys::Array::new();
302        blob_parts.push(&js_string);
303
304        let blob = web_sys::Blob::new_with_blob_sequence_and_options(&blob_parts, &options)
305            .map_err(|_| WebSysError::BlobCreationFailed)?;
306
307        web_sys::Url::create_object_url_with_blob(&blob).map_err(|_| WebSysError::UrlCreationFailed)
308    }
309
310    /// Revoke a blob URL
311    #[cfg(target_arch = "wasm32")]
312    pub fn revoke(url: &str) -> Result<(), WebSysError> {
313        web_sys::Url::revoke_object_url(url).map_err(|_| WebSysError::UrlRevokeFailed)
314    }
315
316    /// Native fallback
317    #[cfg(not(target_arch = "wasm32"))]
318    pub fn from_js_code(_code: &str) -> Result<String, WebSysError> {
319        Err(WebSysError::NotInBrowser)
320    }
321
322    /// Native fallback
323    #[cfg(not(target_arch = "wasm32"))]
324    pub fn revoke(_url: &str) -> Result<(), WebSysError> {
325        Ok(())
326    }
327}
328
329// ============================================================================
330// Base URL (replaces web_sys::window().location().href())
331// ============================================================================
332
333/// Get the base URL of the current page
334#[cfg(target_arch = "wasm32")]
335#[must_use]
336pub fn get_base_url() -> Option<String> {
337    web_sys::window()
338        .and_then(|w| w.location().href().ok())
339        .and_then(|href| {
340            // Strip filename to get directory
341            href.rsplit_once('/').map(|(base, _)| format!("{}/", base))
342        })
343}
344
345/// Native fallback
346#[cfg(not(target_arch = "wasm32"))]
347#[must_use]
348pub fn get_base_url() -> Option<String> {
349    Some("http://localhost/".to_string())
350}
351
352// ============================================================================
353// Web Worker Creation (replaces web_sys::Worker)
354// ============================================================================
355
356/// Generated web worker handle
357#[cfg(target_arch = "wasm32")]
358pub struct GeneratedWorker {
359    inner: web_sys::Worker,
360    _on_message: wasm_bindgen::closure::Closure<dyn Fn(web_sys::MessageEvent)>,
361}
362
363#[cfg(target_arch = "wasm32")]
364impl GeneratedWorker {
365    /// Create a new worker from JavaScript code
366    pub fn from_code<F>(code: &str, on_message: F) -> Result<Self, WebSysError>
367    where
368        F: Fn(web_sys::MessageEvent) + 'static,
369    {
370        use wasm_bindgen::closure::Closure;
371        use wasm_bindgen::JsCast;
372
373        let worker_url = BlobUrl::from_js_code(code)?;
374
375        let worker_options = web_sys::WorkerOptions::new();
376        worker_options.set_type(web_sys::WorkerType::Module);
377
378        let worker = web_sys::Worker::new_with_options(&worker_url, &worker_options)
379            .map_err(|_| WebSysError::WorkerCreationFailed)?;
380
381        // Revoke the blob URL after worker is created
382        let _ = BlobUrl::revoke(&worker_url);
383
384        // Set up message handler
385        let on_message_closure =
386            Closure::wrap(Box::new(on_message) as Box<dyn Fn(web_sys::MessageEvent)>);
387
388        worker.set_onmessage(Some(on_message_closure.as_ref().unchecked_ref()));
389
390        Ok(Self {
391            inner: worker,
392            _on_message: on_message_closure,
393        })
394    }
395
396    /// Post a message to the worker
397    pub fn post_message(&self, message: &wasm_bindgen::JsValue) -> Result<(), WebSysError> {
398        self.inner
399            .post_message(message)
400            .map_err(|_| WebSysError::PostMessageFailed)
401    }
402
403    /// Terminate the worker
404    pub fn terminate(&self) {
405        self.inner.terminate();
406    }
407}
408
409#[cfg(target_arch = "wasm32")]
410impl fmt::Debug for GeneratedWorker {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        f.debug_struct("GeneratedWorker").finish()
413    }
414}
415
416// ============================================================================
417// Error Types
418// ============================================================================
419
420/// Errors from web_sys operations
421#[derive(Debug, Clone)]
422pub enum WebSysError {
423    /// No window object available
424    NoWindow,
425    /// Event creation failed
426    EventCreationFailed,
427    /// Event dispatch failed
428    DispatchFailed,
429    /// Fetch operation failed
430    FetchFailed,
431    /// Not running in browser
432    NotInBrowser,
433    /// Blob creation failed
434    BlobCreationFailed,
435    /// URL creation failed
436    UrlCreationFailed,
437    /// URL revoke failed
438    UrlRevokeFailed,
439    /// Worker creation failed
440    WorkerCreationFailed,
441    /// Post message failed
442    PostMessageFailed,
443}
444
445impl fmt::Display for WebSysError {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        match self {
448            Self::NoWindow => write!(f, "No window object available"),
449            Self::EventCreationFailed => write!(f, "Failed to create custom event"),
450            Self::DispatchFailed => write!(f, "Failed to dispatch event"),
451            Self::FetchFailed => write!(f, "Fetch operation failed"),
452            Self::NotInBrowser => write!(f, "Not running in browser environment"),
453            Self::BlobCreationFailed => write!(f, "Failed to create blob"),
454            Self::UrlCreationFailed => write!(f, "Failed to create object URL"),
455            Self::UrlRevokeFailed => write!(f, "Failed to revoke object URL"),
456            Self::WorkerCreationFailed => write!(f, "Failed to create web worker"),
457            Self::PostMessageFailed => write!(f, "Failed to post message to worker"),
458        }
459    }
460}
461
462impl std::error::Error for WebSysError {}
463
464// ============================================================================
465// Code Generation Metadata
466// ============================================================================
467
468/// Marker trait for generated web_sys code
469///
470/// All generated web_sys code implements this trait for traceability
471pub trait GeneratedWebSys {
472    /// Source brick that generated this code
473    fn source_brick() -> &'static str;
474
475    /// Generation timestamp
476    fn generated_at() -> &'static str;
477}
478
479/// Metadata about generated code
480#[derive(Debug, Clone)]
481pub struct GenerationMetadata {
482    /// Source specification
483    pub spec: &'static str,
484    /// Ticket reference
485    pub ticket: &'static str,
486    /// Generation method
487    pub method: &'static str,
488}
489
490/// Standard generation metadata for this module
491pub const GENERATION_METADATA: GenerationMetadata = GenerationMetadata {
492    spec: "PROBAR-SPEC-009-P7",
493    ticket: "PROBAR-WEBSYS-001",
494    method: "probar::brick::web_sys_gen",
495};
496
497// ============================================================================
498// Tests
499// ============================================================================
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn test_performance_timing_native() {
507        let t1 = PerformanceTiming::now();
508        std::thread::sleep(std::time::Duration::from_millis(10));
509        let t2 = PerformanceTiming::now();
510
511        assert!(t2 > t1);
512    }
513
514    #[test]
515    fn test_performance_measure() {
516        let (result, duration) = PerformanceTiming::measure(|| {
517            std::thread::sleep(std::time::Duration::from_millis(5));
518            42
519        });
520
521        assert_eq!(result, 42);
522        assert!(duration >= 4.0); // Allow some slack
523    }
524
525    #[test]
526    fn test_custom_event_dispatcher() {
527        let dispatcher = CustomEventDispatcher::new("test-event");
528        // Native fallback returns Ok
529        assert!(dispatcher.dispatch().is_ok());
530    }
531
532    #[test]
533    fn test_event_detail_variants() {
534        let string = EventDetail::string("hello");
535        assert!(matches!(string, EventDetail::String(_)));
536
537        let number = EventDetail::number(42.0);
538        assert!(matches!(number, EventDetail::Number(_)));
539
540        let json = EventDetail::json(&vec![1, 2, 3]);
541        assert!(matches!(json, EventDetail::Json(_)));
542    }
543
544    #[test]
545    fn test_fetch_client_native_fallback() {
546        let client = FetchClient::new();
547        // Can't actually test async in sync test, but verify it compiles
548        let _ = client;
549    }
550
551    #[test]
552    fn test_blob_url_native_fallback() {
553        let result = BlobUrl::from_js_code("console.log('test')");
554        assert!(matches!(result, Err(WebSysError::NotInBrowser)));
555
556        // Revoke should succeed (no-op)
557        assert!(BlobUrl::revoke("blob:test").is_ok());
558    }
559
560    #[test]
561    fn test_get_base_url_native() {
562        let url = get_base_url();
563        assert!(url.is_some());
564        assert!(url.unwrap().starts_with("http"));
565    }
566
567    #[test]
568    fn test_web_sys_error_display() {
569        let err = WebSysError::NoWindow;
570        assert_eq!(format!("{}", err), "No window object available");
571    }
572
573    #[test]
574    fn test_generation_metadata() {
575        assert_eq!(GENERATION_METADATA.spec, "PROBAR-SPEC-009-P7");
576        assert_eq!(GENERATION_METADATA.ticket, "PROBAR-WEBSYS-001");
577    }
578
579    // ========================================================================
580    // Additional tests for 95%+ coverage
581    // ========================================================================
582
583    #[test]
584    fn test_web_sys_error_all_variants_display() {
585        // Test all WebSysError variants for Display implementation
586        let errors = [
587            (WebSysError::NoWindow, "No window object available"),
588            (
589                WebSysError::EventCreationFailed,
590                "Failed to create custom event",
591            ),
592            (WebSysError::DispatchFailed, "Failed to dispatch event"),
593            (WebSysError::FetchFailed, "Fetch operation failed"),
594            (
595                WebSysError::NotInBrowser,
596                "Not running in browser environment",
597            ),
598            (WebSysError::BlobCreationFailed, "Failed to create blob"),
599            (
600                WebSysError::UrlCreationFailed,
601                "Failed to create object URL",
602            ),
603            (WebSysError::UrlRevokeFailed, "Failed to revoke object URL"),
604            (
605                WebSysError::WorkerCreationFailed,
606                "Failed to create web worker",
607            ),
608            (
609                WebSysError::PostMessageFailed,
610                "Failed to post message to worker",
611            ),
612        ];
613
614        for (error, expected_msg) in errors {
615            assert_eq!(format!("{}", error), expected_msg);
616        }
617    }
618
619    #[test]
620    fn test_web_sys_error_debug() {
621        let err = WebSysError::NoWindow;
622        let debug_str = format!("{:?}", err);
623        assert!(debug_str.contains("NoWindow"));
624    }
625
626    #[test]
627    fn test_web_sys_error_clone() {
628        let err = WebSysError::FetchFailed;
629        let cloned = err;
630        assert!(matches!(cloned, WebSysError::FetchFailed));
631    }
632
633    #[test]
634    fn test_web_sys_error_std_error_trait() {
635        let err: Box<dyn std::error::Error> = Box::new(WebSysError::NoWindow);
636        // Verify it can be used as a trait object
637        let _ = err.to_string();
638    }
639
640    #[test]
641    fn test_dispatch_with_detail_native_fallback() {
642        let dispatcher = CustomEventDispatcher::new("test-event");
643
644        // Test all EventDetail variants through dispatch_with_detail
645        assert!(dispatcher.dispatch_with_detail(EventDetail::None).is_ok());
646        assert!(dispatcher
647            .dispatch_with_detail(EventDetail::String("hello".to_string()))
648            .is_ok());
649        assert!(dispatcher
650            .dispatch_with_detail(EventDetail::Number(42.0))
651            .is_ok());
652        assert!(dispatcher
653            .dispatch_with_detail(EventDetail::Bool(true))
654            .is_ok());
655        assert!(dispatcher
656            .dispatch_with_detail(EventDetail::Json(r#"{"key":"value"}"#.to_string()))
657            .is_ok());
658    }
659
660    #[test]
661    fn test_event_detail_none() {
662        let detail = EventDetail::None;
663        assert!(matches!(detail, EventDetail::None));
664    }
665
666    #[test]
667    fn test_event_detail_bool() {
668        let detail_true = EventDetail::Bool(true);
669        let detail_false = EventDetail::Bool(false);
670        assert!(matches!(detail_true, EventDetail::Bool(true)));
671        assert!(matches!(detail_false, EventDetail::Bool(false)));
672    }
673
674    #[test]
675    fn test_event_detail_debug() {
676        let detail = EventDetail::String("test".to_string());
677        let debug_str = format!("{:?}", detail);
678        assert!(debug_str.contains("String"));
679    }
680
681    #[test]
682    fn test_event_detail_clone() {
683        let detail = EventDetail::Number(42.0);
684        let cloned = detail;
685        assert!(matches!(cloned, EventDetail::Number(n) if (n - 42.0).abs() < f64::EPSILON));
686    }
687
688    #[test]
689    fn test_event_detail_json_serialization_failure() {
690        // Test the error path when serialization fails
691        // Create a type that always fails to serialize
692        struct FailsToSerialize;
693
694        impl serde::Serialize for FailsToSerialize {
695            fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
696            where
697                S: serde::Serializer,
698            {
699                Err(serde::ser::Error::custom("intentional failure"))
700            }
701        }
702
703        let json = EventDetail::json(&FailsToSerialize);
704        // Should return None when serialization fails
705        assert!(matches!(json, EventDetail::None));
706
707        // Also verify the happy path still works
708        let json_ok = EventDetail::json(&"simple string");
709        assert!(matches!(json_ok, EventDetail::Json(_)));
710    }
711
712    #[tokio::test]
713    async fn test_fetch_bytes_native_fallback() {
714        // Test the async fetch_bytes method in native mode
715        let client = FetchClient::new();
716
717        let result = client.fetch_bytes("http://example.com").await;
718
719        // Native fallback should return NotInBrowser error
720        assert!(matches!(result, Err(WebSysError::NotInBrowser)));
721    }
722
723    #[test]
724    fn test_fetch_client_default() {
725        let client = FetchClient;
726        let debug_str = format!("{:?}", client);
727        assert!(debug_str.contains("FetchClient"));
728    }
729
730    #[test]
731    fn test_fetch_client_clone() {
732        let client = FetchClient::new();
733        let cloned = client;
734        let _ = format!("{:?}", cloned);
735    }
736
737    #[test]
738    fn test_fetch_result_struct() {
739        let result = FetchResult {
740            status: 200,
741            body: vec![1, 2, 3, 4],
742        };
743        assert_eq!(result.status, 200);
744        assert_eq!(result.body.len(), 4);
745
746        // Test Debug
747        let debug_str = format!("{:?}", result);
748        assert!(debug_str.contains("FetchResult"));
749        assert!(debug_str.contains("200"));
750    }
751
752    #[test]
753    fn test_custom_event_dispatcher_debug() {
754        let dispatcher = CustomEventDispatcher::new("my-event");
755        let debug_str = format!("{:?}", dispatcher);
756        assert!(debug_str.contains("CustomEventDispatcher"));
757    }
758
759    #[test]
760    fn test_custom_event_dispatcher_clone() {
761        let dispatcher = CustomEventDispatcher::new("clone-test");
762        let cloned = dispatcher;
763        assert!(cloned.dispatch().is_ok());
764    }
765
766    #[test]
767    fn test_performance_timing_debug() {
768        let timing = PerformanceTiming;
769        let debug_str = format!("{:?}", timing);
770        assert!(debug_str.contains("PerformanceTiming"));
771    }
772
773    #[test]
774    fn test_performance_timing_clone_copy() {
775        let timing1 = PerformanceTiming;
776        let timing2 = timing1; // Copy
777        let timing3 = timing1; // Clone
778        let _ = format!("{:?}", timing2);
779        let _ = format!("{:?}", timing3);
780    }
781
782    #[test]
783    fn test_blob_url_debug() {
784        let blob = BlobUrl;
785        let debug_str = format!("{:?}", blob);
786        assert!(debug_str.contains("BlobUrl"));
787    }
788
789    #[test]
790    fn test_blob_url_clone() {
791        let blob1 = BlobUrl;
792        let blob2 = blob1;
793        let _ = format!("{:?}", blob2);
794    }
795
796    #[test]
797    fn test_generation_metadata_debug() {
798        let debug_str = format!("{:?}", GENERATION_METADATA);
799        assert!(debug_str.contains("GenerationMetadata"));
800        assert!(debug_str.contains("PROBAR-SPEC-009-P7"));
801    }
802
803    #[test]
804    fn test_generation_metadata_clone() {
805        let cloned = GENERATION_METADATA.clone();
806        assert_eq!(cloned.spec, GENERATION_METADATA.spec);
807        assert_eq!(cloned.ticket, GENERATION_METADATA.ticket);
808        assert_eq!(cloned.method, GENERATION_METADATA.method);
809    }
810
811    #[test]
812    fn test_generation_metadata_method_field() {
813        assert_eq!(GENERATION_METADATA.method, "probar::brick::web_sys_gen");
814    }
815
816    #[test]
817    fn test_get_base_url_native_format() {
818        let url = get_base_url().expect("should return Some in native mode");
819        assert_eq!(url, "http://localhost/");
820    }
821
822    #[test]
823    fn test_performance_timing_now_returns_positive() {
824        let now = PerformanceTiming::now();
825        assert!(now > 0.0, "Timestamp should be positive");
826    }
827
828    #[test]
829    fn test_performance_timing_monotonic() {
830        let t1 = PerformanceTiming::now();
831        let t2 = PerformanceTiming::now();
832        let t3 = PerformanceTiming::now();
833        assert!(t2 >= t1);
834        assert!(t3 >= t2);
835    }
836
837    #[test]
838    fn test_event_detail_string_with_into() {
839        // Test that Into<String> works with various types
840        let s1 = EventDetail::string("literal str");
841        let s2 = EventDetail::string(String::from("String type"));
842
843        match s1 {
844            EventDetail::String(s) => assert_eq!(s, "literal str"),
845            _ => panic!("Expected String variant"),
846        }
847
848        match s2 {
849            EventDetail::String(s) => assert_eq!(s, "String type"),
850            _ => panic!("Expected String variant"),
851        }
852    }
853
854    #[test]
855    fn test_event_detail_number_special_values() {
856        // Test special floating point values
857        let nan = EventDetail::number(f64::NAN);
858        let inf = EventDetail::number(f64::INFINITY);
859        let neg_inf = EventDetail::number(f64::NEG_INFINITY);
860        let zero = EventDetail::number(0.0);
861        let neg_zero = EventDetail::number(-0.0);
862
863        assert!(matches!(nan, EventDetail::Number(_)));
864        assert!(matches!(inf, EventDetail::Number(_)));
865        assert!(matches!(neg_inf, EventDetail::Number(_)));
866        assert!(matches!(zero, EventDetail::Number(_)));
867        assert!(matches!(neg_zero, EventDetail::Number(_)));
868    }
869
870    #[test]
871    fn test_event_detail_json_complex_structures() {
872        use std::collections::HashMap;
873
874        // Test with nested HashMap
875        let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
876        map.insert("numbers", vec![1, 2, 3]);
877
878        let json = EventDetail::json(&map);
879        match json {
880            EventDetail::Json(s) => {
881                assert!(s.contains("numbers"));
882                assert!(s.contains("[1,2,3]"));
883            }
884            _ => panic!("Expected Json variant"),
885        }
886    }
887
888    #[test]
889    fn test_custom_event_dispatcher_new_with_string() {
890        let dispatcher1 = CustomEventDispatcher::new("event-name");
891        let dispatcher2 = CustomEventDispatcher::new(String::from("event-name-string"));
892        assert!(dispatcher1.dispatch().is_ok());
893        assert!(dispatcher2.dispatch().is_ok());
894    }
895
896    #[test]
897    fn test_blob_url_revoke_empty_string() {
898        // Revoke with empty string should still succeed in native fallback
899        assert!(BlobUrl::revoke("").is_ok());
900    }
901
902    #[test]
903    fn test_blob_url_from_js_code_empty() {
904        // Empty JS code should still return NotInBrowser in native
905        let result = BlobUrl::from_js_code("");
906        assert!(matches!(result, Err(WebSysError::NotInBrowser)));
907    }
908
909    #[test]
910    fn test_performance_measure_with_panic_safe() {
911        // Test measure with a quick operation
912        let (result, duration) = PerformanceTiming::measure(|| {
913            let mut sum = 0u64;
914            for i in 0..1000 {
915                sum = sum.wrapping_add(i);
916            }
917            sum
918        });
919
920        assert!(result > 0);
921        assert!(duration >= 0.0);
922    }
923
924    #[test]
925    fn test_fetch_result_empty_body() {
926        let result = FetchResult {
927            status: 204,
928            body: vec![],
929        };
930        assert_eq!(result.status, 204);
931        assert!(result.body.is_empty());
932    }
933
934    #[test]
935    fn test_fetch_result_large_body() {
936        let result = FetchResult {
937            status: 200,
938            body: vec![0u8; 10000],
939        };
940        assert_eq!(result.body.len(), 10000);
941    }
942
943    #[test]
944    fn test_web_sys_error_is_send_sync() {
945        fn assert_send<T: Send>() {}
946        fn assert_sync<T: Sync>() {}
947
948        assert_send::<WebSysError>();
949        assert_sync::<WebSysError>();
950    }
951
952    /// Test implementing GeneratedWebSys trait
953    struct TestGeneratedCode;
954
955    impl GeneratedWebSys for TestGeneratedCode {
956        fn source_brick() -> &'static str {
957            "test-brick"
958        }
959
960        fn generated_at() -> &'static str {
961            "2024-01-01T00:00:00Z"
962        }
963    }
964
965    #[test]
966    fn test_generated_websys_trait() {
967        assert_eq!(TestGeneratedCode::source_brick(), "test-brick");
968        assert_eq!(TestGeneratedCode::generated_at(), "2024-01-01T00:00:00Z");
969    }
970
971    #[test]
972    fn test_all_event_detail_variants_in_match() {
973        let variants = vec![
974            EventDetail::None,
975            EventDetail::String("test".to_string()),
976            EventDetail::Number(1.5),
977            EventDetail::Bool(false),
978            EventDetail::Json("{}".to_string()),
979        ];
980
981        for variant in variants {
982            let _ = match &variant {
983                EventDetail::None => "none",
984                EventDetail::String(_) => "string",
985                EventDetail::Number(_) => "number",
986                EventDetail::Bool(_) => "bool",
987                EventDetail::Json(_) => "json",
988            };
989        }
990    }
991}