1use std::fmt;
25
26#[derive(Debug, Clone, Copy)]
37pub struct PerformanceTiming;
38
39impl PerformanceTiming {
40 #[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 #[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 #[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#[derive(Debug, Clone)]
82pub enum EventDetail {
83 None,
85 String(String),
87 Number(f64),
89 Bool(bool),
91 Json(String),
93}
94
95impl EventDetail {
96 #[must_use]
98 pub fn string(s: impl Into<String>) -> Self {
99 Self::String(s.into())
100 }
101
102 #[must_use]
104 pub fn number(n: f64) -> Self {
105 Self::Number(n)
106 }
107
108 #[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#[derive(Debug, Clone)]
128pub struct CustomEventDispatcher {
129 #[allow(dead_code)] event_name: String,
131}
132
133impl CustomEventDispatcher {
134 #[must_use]
136 pub fn new(event_name: impl Into<String>) -> Self {
137 Self {
138 event_name: event_name.into(),
139 }
140 }
141
142 #[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 #[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 #[cfg(not(target_arch = "wasm32"))]
186 pub fn dispatch(&self) -> Result<bool, WebSysError> {
187 Ok(true)
188 }
189
190 #[cfg(not(target_arch = "wasm32"))]
192 pub fn dispatch_with_detail(&self, _detail: EventDetail) -> Result<bool, WebSysError> {
193 Ok(true)
194 }
195}
196
197#[derive(Debug)]
203pub struct FetchResult {
204 pub status: u16,
206 pub body: Vec<u8>,
208}
209
210#[derive(Debug, Clone, Default)]
214pub struct FetchClient;
215
216impl FetchClient {
217 #[must_use]
219 pub fn new() -> Self {
220 Self
221 }
222
223 #[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 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 #[cfg(not(target_arch = "wasm32"))]
264 #[allow(clippy::unused_async)] pub async fn fetch_bytes(&self, _url: &str) -> Result<Vec<u8>, WebSysError> {
266 Err(WebSysError::NotInBrowser)
267 }
268}
269
270#[derive(Debug, Clone)]
284pub struct BlobUrl;
285
286impl BlobUrl {
287 #[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 #[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 #[cfg(not(target_arch = "wasm32"))]
313 pub fn from_js_code(_code: &str) -> Result<String, WebSysError> {
314 Err(WebSysError::NotInBrowser)
315 }
316
317 #[cfg(not(target_arch = "wasm32"))]
319 pub fn revoke(_url: &str) -> Result<(), WebSysError> {
320 Ok(())
321 }
322}
323
324#[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 href.rsplit_once('/').map(|(base, _)| format!("{}/", base))
337 })
338}
339
340#[cfg(not(target_arch = "wasm32"))]
342#[must_use]
343pub fn get_base_url() -> Option<String> {
344 Some("http://localhost/".to_string())
345}
346
347#[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 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 let _ = BlobUrl::revoke(&worker_url);
378
379 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 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 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#[derive(Debug, Clone)]
417pub enum WebSysError {
418 NoWindow,
420 EventCreationFailed,
422 DispatchFailed,
424 FetchFailed,
426 NotInBrowser,
428 BlobCreationFailed,
430 UrlCreationFailed,
432 UrlRevokeFailed,
434 WorkerCreationFailed,
436 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
459pub trait GeneratedWebSys {
467 fn source_brick() -> &'static str;
469
470 fn generated_at() -> &'static str;
472}
473
474#[derive(Debug, Clone)]
476pub struct GenerationMetadata {
477 pub spec: &'static str,
479 pub ticket: &'static str,
481 pub method: &'static str,
483}
484
485pub 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#[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); }
519
520 #[test]
521 fn test_custom_event_dispatcher() {
522 let dispatcher = CustomEventDispatcher::new("test-event");
523 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 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 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 #[test]
579 fn test_web_sys_error_all_variants_display() {
580 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 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 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 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 assert!(matches!(json, EventDetail::None));
701
702 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 let client = FetchClient::new();
711
712 let result = client.fetch_bytes("http://example.com").await;
713
714 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 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; let timing3 = timing1; 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 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 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 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 assert!(BlobUrl::revoke("").is_ok());
895 }
896
897 #[test]
898 fn test_blob_url_from_js_code_empty() {
899 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 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 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}