1use alloc::collections::btree_map::BTreeMap;
24use alloc::vec::Vec;
25
26use azul_core::dom::DomNodeId;
27use azul_core::events::{
28 EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
29};
30use azul_core::task::Instant;
31
32pub use azul_core::geolocation::{GeolocationProbeConfig, LocationFix};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[repr(C, u8)]
44pub enum GeolocationDiffEvent {
45 Subscribe { config: GeolocationProbeConfig },
48 Release,
50 Reconfigure { config: GeolocationProbeConfig },
54}
55
56#[derive(Debug, Clone, PartialEq, Default)]
59pub struct GeolocationManager {
60 pub latest_fix: Option<LocationFix>,
63 pub active_config: Option<GeolocationProbeConfig>,
66 pending_events: Vec<GeolocationDiffEvent>,
68 refcount: u32,
70 pending_event: bool,
76 pub last_error: Option<LocationError>,
79 pending_error_event: bool,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Default)]
91pub struct LocationError {
92 pub code: u32,
94 pub message: String,
96}
97
98impl GeolocationManager {
99 #[must_use] pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use] pub const fn latest_fix(&self) -> Option<LocationFix> {
104 self.latest_fix
105 }
106
107 #[must_use] pub const fn refcount(&self) -> u32 {
108 self.refcount
109 }
110
111 pub fn set_latest_fix(&mut self, fix: LocationFix) -> bool {
120 let changed = self
121 .latest_fix
122 .is_none_or(|prev| !Self::location_fix_bitwise_eq(&prev, &fix));
123 self.latest_fix = Some(fix);
124 if changed {
125 self.pending_event = true;
126 self.last_error = None;
128 }
129 changed
130 }
131
132 pub fn set_last_error(&mut self, error: LocationError) {
136 self.last_error = Some(error);
137 self.pending_error_event = true;
138 }
139
140 pub const fn clear_pending_event(&mut self) {
143 self.pending_event = false;
144 self.pending_error_event = false;
145 }
146
147 #[must_use] pub const fn has_active_subscription(&self) -> bool {
152 self.refcount > 0
153 }
154
155 const fn location_fix_bitwise_eq(a: &LocationFix, b: &LocationFix) -> bool {
156 a.latitude_deg.to_bits() == b.latitude_deg.to_bits()
157 && a.longitude_deg.to_bits() == b.longitude_deg.to_bits()
158 && a.accuracy_m.to_bits() == b.accuracy_m.to_bits()
159 && a.altitude_m.to_bits() == b.altitude_m.to_bits()
160 && a.altitude_accuracy_m.to_bits() == b.altitude_accuracy_m.to_bits()
161 && a.heading_deg.to_bits() == b.heading_deg.to_bits()
162 && a.speed_mps.to_bits() == b.speed_mps.to_bits()
163 && a.timestamp_ms == b.timestamp_ms
164 }
165
166 pub fn take_pending_events(&mut self) -> Vec<GeolocationDiffEvent> {
169 core::mem::take(&mut self.pending_events)
170 }
171
172 pub fn diff_layout<F>(&mut self, mut for_each_probe: F)
178 where
179 F: FnMut(&mut dyn FnMut(GeolocationProbeConfig)),
180 {
181 let mut new_count: u32 = 0;
182 let mut next_config: Option<GeolocationProbeConfig> = None;
183 for_each_probe(&mut |cfg| {
184 new_count += 1;
185 if next_config.is_none() {
190 next_config = Some(cfg);
191 }
192 });
193
194 let old_count = self.refcount;
195 self.refcount = new_count;
196
197 match (old_count, new_count) {
198 (0, n) if n > 0 => {
199 let config = next_config.unwrap_or_default();
200 self.active_config = Some(config);
201 self.pending_events
202 .push(GeolocationDiffEvent::Subscribe { config });
203 }
204 (m, 0) if m > 0 => {
205 self.active_config = None;
206 self.latest_fix = None;
207 self.pending_events.push(GeolocationDiffEvent::Release);
208 }
209 (m, n) if m > 0 && n > 0 => {
210 let new_config = next_config.unwrap_or_default();
213 if Some(new_config) != self.active_config {
214 self.active_config = Some(new_config);
215 self.pending_events
216 .push(GeolocationDiffEvent::Reconfigure { config: new_config });
217 }
218 }
219 _ => {
220 }
222 }
223 }
224}
225
226impl EventProvider for GeolocationManager {
227 fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
233 let mut events = Vec::new();
234 if self.pending_event {
235 events.push(SyntheticEvent::new(
236 EventType::GeolocationFix,
237 CoreEventSource::User,
238 DomNodeId::ROOT,
239 timestamp.clone(),
240 EventData::None,
241 ));
242 }
243 if self.pending_error_event {
244 events.push(SyntheticEvent::new(
245 EventType::GeolocationError,
246 CoreEventSource::User,
247 DomNodeId::ROOT,
248 timestamp,
249 EventData::None,
250 ));
251 }
252 events
253 }
254}
255
256static PENDING_FIXES: std::sync::Mutex<Vec<LocationFix>> =
269 std::sync::Mutex::new(Vec::new());
270
271pub fn push_location_fix(fix: LocationFix) {
275 let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
276 q.push(fix);
277}
278
279pub fn drain_location_fixes() -> Vec<LocationFix> {
283 let mut q = PENDING_FIXES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
284 core::mem::take(&mut *q)
285}
286
287static PENDING_ERRORS: std::sync::Mutex<Vec<LocationError>> =
292 std::sync::Mutex::new(Vec::new());
293
294pub fn push_location_error(error: LocationError) {
297 let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
298 q.push(error);
299}
300
301pub fn drain_location_errors() -> Vec<LocationError> {
303 let mut q = PENDING_ERRORS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
304 core::mem::take(&mut *q)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 fn cfg() -> GeolocationProbeConfig {
312 GeolocationProbeConfig::default()
313 }
314
315 fn high_accuracy_cfg() -> GeolocationProbeConfig {
316 GeolocationProbeConfig {
317 high_accuracy: true,
318 ..GeolocationProbeConfig::default()
319 }
320 }
321
322 fn fix(lat: f64, lon: f64) -> LocationFix {
323 LocationFix {
324 latitude_deg: lat,
325 longitude_deg: lon,
326 accuracy_m: 10.0,
327 altitude_m: f32::NAN,
328 altitude_accuracy_m: f32::NAN,
329 heading_deg: f32::NAN,
330 speed_mps: f32::NAN,
331 timestamp_ms: 0,
332 }
333 }
334
335 #[test]
336 fn first_probe_emits_subscribe_with_config() {
337 let mut mgr = GeolocationManager::new();
338 mgr.diff_layout(|emit| emit(cfg()));
339 assert_eq!(mgr.refcount(), 1);
340 let events = mgr.take_pending_events();
341 assert_eq!(events.len(), 1);
342 assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
343 }
344
345 #[test]
346 fn last_probe_drop_emits_release_and_clears_fix() {
347 let mut mgr = GeolocationManager::new();
348 mgr.diff_layout(|emit| emit(cfg()));
349 mgr.set_latest_fix(fix(37.0, -122.0));
350 drop(mgr.take_pending_events());
351
352 mgr.diff_layout(|_emit| {});
353 assert_eq!(mgr.refcount(), 0);
354 assert_eq!(mgr.latest_fix(), None);
355 let events = mgr.take_pending_events();
356 assert_eq!(events.len(), 1);
357 assert!(matches!(events[0], GeolocationDiffEvent::Release));
358 }
359
360 #[test]
361 fn config_drift_emits_reconfigure() {
362 let mut mgr = GeolocationManager::new();
363 mgr.diff_layout(|emit| emit(cfg()));
364 drop(mgr.take_pending_events());
365
366 mgr.diff_layout(|emit| emit(high_accuracy_cfg()));
367 let events = mgr.take_pending_events();
368 assert_eq!(events.len(), 1);
369 let ev = &events[0];
370 match ev {
371 GeolocationDiffEvent::Reconfigure { config } => {
372 assert!(config.high_accuracy);
373 }
374 _ => panic!("expected Reconfigure, got {ev:?}"),
375 }
376 }
377
378 #[test]
379 fn stable_config_does_not_re_emit() {
380 let mut mgr = GeolocationManager::new();
381 mgr.diff_layout(|emit| emit(cfg()));
382 drop(mgr.take_pending_events());
383
384 mgr.diff_layout(|emit| emit(cfg()));
386 assert!(mgr.take_pending_events().is_empty());
387 }
388
389 #[test]
390 fn set_latest_fix_returns_change_flag() {
391 let mut mgr = GeolocationManager::new();
392 assert!(mgr.set_latest_fix(fix(37.0, -122.0)));
393 assert!(!mgr.set_latest_fix(fix(37.0, -122.0)));
394 assert!(mgr.set_latest_fix(fix(37.7749, -122.4194)));
395 }
396
397 #[test]
398 fn missing_fields_decode_to_none() {
399 let f = fix(0.0, 0.0);
400 assert_eq!(f.altitude(), None);
401 assert_eq!(f.heading(), None);
402 assert_eq!(f.speed(), None);
403 }
404
405 #[test]
406 fn provider_yields_fix_event_then_clears() {
407 use azul_core::task::{Instant, SystemTick};
408
409 let ts = Instant::Tick(SystemTick::new(0));
410 let mut mgr = GeolocationManager::new();
411 assert!(mgr.get_pending_events(ts.clone()).is_empty(), "no fix yet");
412
413 mgr.set_latest_fix(fix(37.0, -122.0));
414 let events = mgr.get_pending_events(ts.clone());
415 assert_eq!(events.len(), 1);
416 assert_eq!(events[0].event_type, EventType::GeolocationFix);
417
418 mgr.clear_pending_event();
419 assert!(mgr.get_pending_events(ts.clone()).is_empty(), "cleared after dispatch");
420
421 mgr.set_latest_fix(fix(37.0, -122.0));
423 assert!(mgr.get_pending_events(ts).is_empty());
424 }
425
426 #[test]
427 fn error_channel_and_provider_event() {
428 use azul_core::task::{Instant, SystemTick};
429
430 drop(drain_location_errors());
431 push_location_error(LocationError { code: 1, message: "denied".into() });
432 let errs = drain_location_errors();
433 assert_eq!(errs.len(), 1);
434 assert!(drain_location_errors().is_empty());
435
436 let ts = Instant::Tick(SystemTick::new(0));
437 let mut mgr = GeolocationManager::new();
438 mgr.set_last_error(errs[0].clone());
439 let events = mgr.get_pending_events(ts.clone());
440 assert_eq!(events.len(), 1);
441 assert_eq!(
442 events[0].event_type,
443 EventType::GeolocationError
444 );
445 mgr.clear_pending_event();
446 assert!(mgr.get_pending_events(ts).is_empty());
447
448 mgr.set_latest_fix(fix(1.0, 2.0));
450 assert!(mgr.last_error.is_none());
451 }
452
453 #[test]
454 fn subscription_flag_follows_probe_refcount() {
455 let mut mgr = GeolocationManager::new();
456 assert!(!mgr.has_active_subscription());
457 mgr.diff_layout(|emit| emit(cfg()));
458 assert!(mgr.has_active_subscription());
459 mgr.diff_layout(|_emit| {});
460 assert!(!mgr.has_active_subscription());
461 }
462
463 #[test]
464 #[allow(clippy::float_cmp)] fn async_fixes_round_trip_through_manager() {
466 drop(drain_location_fixes());
468
469 push_location_fix(fix(37.0, -122.0));
470 push_location_fix(fix(48.8566, 2.3522)); let drained = drain_location_fixes();
472 assert_eq!(drained.len(), 2, "both parked fixes drain in order");
473 assert_eq!(drained[0].latitude_deg, 37.0);
474 assert_eq!(drained[1].latitude_deg, 48.8566);
475
476 let mut mgr = GeolocationManager::new();
478 for f in &drained {
479 mgr.set_latest_fix(*f);
480 }
481 let got = mgr.latest_fix().expect("a fix was applied");
482 assert_eq!(got.latitude_deg, 48.8566, "the last applied fix wins");
483
484 assert!(drain_location_fixes().is_empty());
486 }
487}
488
489#[cfg(test)]
490#[allow(clippy::float_cmp, clippy::eq_op, clippy::redundant_clone)]
491mod autotest_generated {
492 use azul_core::task::{Instant, SystemTick};
493
494 use super::*;
495
496 const fn full_fix() -> LocationFix {
501 LocationFix {
502 latitude_deg: 48.208_8,
503 longitude_deg: 16.372_1,
504 accuracy_m: 5.0,
505 altitude_m: 171.0,
506 altitude_accuracy_m: 3.0,
507 heading_deg: 90.0,
508 speed_mps: 1.5,
509 timestamp_ms: 1_234,
510 }
511 }
512
513 const fn nan_fix() -> LocationFix {
516 LocationFix {
517 latitude_deg: 37.0,
518 longitude_deg: -122.0,
519 accuracy_m: 10.0,
520 altitude_m: f32::NAN,
521 altitude_accuracy_m: f32::NAN,
522 heading_deg: f32::NAN,
523 speed_mps: f32::NAN,
524 timestamp_ms: 0,
525 }
526 }
527
528 const fn probe_cfg(high_accuracy: bool, max_accuracy_m: f32) -> GeolocationProbeConfig {
529 GeolocationProbeConfig {
530 high_accuracy,
531 background: false,
532 max_accuracy_m,
533 min_interval_ms: 0,
534 }
535 }
536
537 fn tick() -> Instant {
538 Instant::Tick(SystemTick::new(0))
539 }
540
541 fn single_field_mutations() -> Vec<(&'static str, LocationFix)> {
544 let base = full_fix();
545 let mut out = Vec::new();
546
547 let mut f = base;
548 f.latitude_deg = -48.208_8;
549 out.push(("latitude_deg", f));
550
551 let mut f = base;
552 f.longitude_deg = 16.372_2;
553 out.push(("longitude_deg", f));
554
555 let mut f = base;
556 f.accuracy_m = 5.000_001;
557 out.push(("accuracy_m", f));
558
559 let mut f = base;
560 f.altitude_m = f32::NAN;
561 out.push(("altitude_m", f));
562
563 let mut f = base;
564 f.altitude_accuracy_m = f32::NAN;
565 out.push(("altitude_accuracy_m", f));
566
567 let mut f = base;
568 f.heading_deg = 270.0;
569 out.push(("heading_deg", f));
570
571 let mut f = base;
572 f.speed_mps = -1.5;
573 out.push(("speed_mps", f));
574
575 let mut f = base;
576 f.timestamp_ms = u64::MAX;
577 out.push(("timestamp_ms", f));
578
579 out
580 }
581
582 #[test]
585 fn new_equals_default_and_holds_construction_invariants() {
586 let mgr = GeolocationManager::new();
587 assert_eq!(mgr, GeolocationManager::default());
588 assert_eq!(mgr, GeolocationManager::new(), "construction is deterministic");
589
590 assert_eq!(mgr.latest_fix(), None);
591 assert_eq!(mgr.refcount(), 0);
592 assert_eq!(mgr.active_config, None);
593 assert_eq!(mgr.last_error, None);
594 assert!(!mgr.has_active_subscription());
595 assert!(mgr.get_pending_events(tick()).is_empty());
596 }
597
598 #[test]
599 fn getters_on_a_fresh_manager_do_not_panic() {
600 let mut mgr = GeolocationManager::new();
601 for _ in 0..3 {
603 assert_eq!(mgr.latest_fix(), None);
604 assert_eq!(mgr.refcount(), 0);
605 assert!(!mgr.has_active_subscription());
606 assert!(mgr.take_pending_events().is_empty());
607 mgr.clear_pending_event();
608 }
609 }
610
611 #[test]
612 fn latest_fix_round_trips_extreme_bit_patterns() {
613 let extreme = LocationFix {
614 latitude_deg: f64::INFINITY,
615 longitude_deg: f64::NEG_INFINITY,
616 accuracy_m: f32::MAX,
617 altitude_m: f32::MIN_POSITIVE,
618 altitude_accuracy_m: f32::from_bits(1), heading_deg: -0.0,
620 speed_mps: f32::NEG_INFINITY,
621 timestamp_ms: u64::MAX,
622 };
623
624 let mut mgr = GeolocationManager::new();
625 assert!(mgr.set_latest_fix(extreme), "first fix is always a change");
626
627 let got = mgr.latest_fix().expect("a fix was stored");
628 assert_eq!(got.latitude_deg.to_bits(), extreme.latitude_deg.to_bits());
630 assert_eq!(got.longitude_deg.to_bits(), extreme.longitude_deg.to_bits());
631 assert_eq!(got.accuracy_m.to_bits(), extreme.accuracy_m.to_bits());
632 assert_eq!(got.altitude_m.to_bits(), extreme.altitude_m.to_bits());
633 assert_eq!(
634 got.altitude_accuracy_m.to_bits(),
635 extreme.altitude_accuracy_m.to_bits()
636 );
637 assert_eq!(got.heading_deg.to_bits(), extreme.heading_deg.to_bits());
638 assert_eq!(got.speed_mps.to_bits(), extreme.speed_mps.to_bits());
639 assert_eq!(got.timestamp_ms, u64::MAX);
640
641 assert_eq!(got.speed(), Some(f32::NEG_INFINITY));
643 assert!(mgr.latest_fix().is_some());
644 assert!(!mgr.set_latest_fix(extreme));
646 }
647
648 #[test]
649 fn nan_fix_round_trips_and_getters_still_report_missing() {
650 let mut mgr = GeolocationManager::new();
651 assert!(mgr.set_latest_fix(nan_fix()));
652
653 let got = mgr.latest_fix().expect("a fix was stored");
654 assert_eq!(got.altitude(), None);
655 assert_eq!(got.altitude_accuracy(), None);
656 assert_eq!(got.heading(), None);
657 assert_eq!(got.speed(), None);
658 assert_eq!(got.latitude_deg.to_bits(), 37.0_f64.to_bits());
659 }
660
661 #[test]
664 fn bitwise_eq_is_reflexive_on_nan_unlike_partial_eq() {
665 let f = nan_fix();
666 assert!(f != f, "PartialEq on a NaN-carrying fix is not reflexive");
669 assert!(GeolocationManager::location_fix_bitwise_eq(&f, &f));
670 assert!(GeolocationManager::location_fix_bitwise_eq(&nan_fix(), &nan_fix()));
671 }
672
673 #[test]
674 fn bitwise_eq_is_reflexive_and_symmetric_over_extremes() {
675 let extremes = [
676 full_fix(),
677 nan_fix(),
678 LocationFix {
679 latitude_deg: f64::INFINITY,
680 longitude_deg: f64::NEG_INFINITY,
681 accuracy_m: f32::INFINITY,
682 altitude_m: f32::MAX,
683 altitude_accuracy_m: f32::MIN,
684 heading_deg: f32::from_bits(1),
685 speed_mps: -0.0,
686 timestamp_ms: u64::MAX,
687 },
688 LocationFix {
689 latitude_deg: 0.0,
690 longitude_deg: 0.0,
691 accuracy_m: 0.0,
692 altitude_m: 0.0,
693 altitude_accuracy_m: 0.0,
694 heading_deg: 0.0,
695 speed_mps: 0.0,
696 timestamp_ms: 0,
697 },
698 ];
699
700 for (i, a) in extremes.iter().enumerate() {
701 assert!(
702 GeolocationManager::location_fix_bitwise_eq(a, a),
703 "not reflexive for extreme #{i}"
704 );
705 for (j, b) in extremes.iter().enumerate() {
706 assert_eq!(
707 GeolocationManager::location_fix_bitwise_eq(a, b),
708 GeolocationManager::location_fix_bitwise_eq(b, a),
709 "not symmetric for ({i}, {j})"
710 );
711 if i != j {
712 assert!(
713 !GeolocationManager::location_fix_bitwise_eq(a, b),
714 "distinct extremes #{i}/#{j} compared equal"
715 );
716 }
717 }
718 }
719 }
720
721 #[test]
722 fn bitwise_eq_looks_at_every_field() {
723 let base = full_fix();
724 for (field, mutated) in single_field_mutations() {
725 assert!(
726 !GeolocationManager::location_fix_bitwise_eq(&base, &mutated),
727 "`{field}` is ignored by location_fix_bitwise_eq"
728 );
729 assert!(
730 !GeolocationManager::location_fix_bitwise_eq(&mutated, &base),
731 "`{field}` comparison is asymmetric"
732 );
733 assert!(GeolocationManager::location_fix_bitwise_eq(&mutated, &mutated));
734 }
735 }
736
737 #[test]
738 fn bitwise_eq_separates_positive_and_negative_zero() {
739 let mut a = full_fix();
740 a.heading_deg = 0.0;
741 let mut b = a;
742 b.heading_deg = -0.0;
743
744 assert!(a.heading_deg == b.heading_deg);
747 assert!(!GeolocationManager::location_fix_bitwise_eq(&a, &b));
748 }
749
750 #[test]
753 fn repeated_identical_nan_fix_reports_no_change() {
754 let mut mgr = GeolocationManager::new();
755 assert!(mgr.set_latest_fix(nan_fix()), "first fix advances");
756 for _ in 0..100 {
757 assert!(
758 !mgr.set_latest_fix(nan_fix()),
759 "an unchanged NaN-carrying fix must not look like movement"
760 );
761 }
762 assert_eq!(mgr.get_pending_events(tick()).len(), 1);
764 }
765
766 #[test]
767 fn set_latest_fix_detects_a_change_in_every_field() {
768 for (field, mutated) in single_field_mutations() {
769 let mut mgr = GeolocationManager::new();
770 assert!(mgr.set_latest_fix(full_fix()));
771 mgr.clear_pending_event();
772
773 assert!(
774 mgr.set_latest_fix(mutated),
775 "a change in `{field}` was not reported"
776 );
777 assert_eq!(
778 mgr.get_pending_events(tick()).len(),
779 1,
780 "a change in `{field}` raised no GeolocationFix event"
781 );
782 }
783 }
784
785 #[test]
786 fn set_latest_fix_survives_extreme_and_pathological_values() {
787 let mut mgr = GeolocationManager::new();
788
789 let pathological = [
792 (f64::MAX, f64::MIN),
793 (f64::INFINITY, f64::NEG_INFINITY),
794 (f64::NAN, f64::NAN),
795 (1e308, -1e308),
796 (f64::MIN_POSITIVE, -f64::MIN_POSITIVE),
797 (91.0, 181.0), (-91.0, -181.0), ];
800
801 for (lat, lon) in pathological {
802 let f = LocationFix {
803 latitude_deg: lat,
804 longitude_deg: lon,
805 accuracy_m: f32::INFINITY,
806 altitude_m: f32::NAN,
807 altitude_accuracy_m: f32::NAN,
808 heading_deg: f32::NAN,
809 speed_mps: f32::NAN,
810 timestamp_ms: u64::MAX,
811 };
812 mgr.set_latest_fix(f);
813 let got = mgr.latest_fix().expect("stored");
814 assert_eq!(got.latitude_deg.to_bits(), lat.to_bits());
815 assert_eq!(got.longitude_deg.to_bits(), lon.to_bits());
816 assert!(!mgr.set_latest_fix(f), "re-applying the same fix is not a change");
819 }
820 }
821
822 #[test]
823 fn timestamp_only_movement_counts_as_a_change() {
824 let mut mgr = GeolocationManager::new();
825 let mut f = nan_fix();
826 assert!(mgr.set_latest_fix(f));
827
828 f.timestamp_ms = u64::MAX;
829 assert!(
830 mgr.set_latest_fix(f),
831 "a fresh sample at the same coordinates is still a new fix"
832 );
833 assert_eq!(mgr.latest_fix().expect("stored").timestamp_ms, u64::MAX);
834 }
835
836 #[test]
837 fn a_changed_fix_supersedes_the_stored_error_but_an_unchanged_one_does_not() {
838 let mut mgr = GeolocationManager::new();
839 assert!(mgr.set_latest_fix(full_fix()));
840
841 mgr.set_last_error(LocationError {
842 code: 2,
843 message: String::from("timed out"),
844 });
845 assert!(mgr.last_error.is_some());
846
847 assert!(!mgr.set_latest_fix(full_fix()));
849 assert!(
850 mgr.last_error.is_some(),
851 "an unchanged fix does not clear the error (documented `changed`-gated behaviour)"
852 );
853
854 assert!(mgr.set_latest_fix(nan_fix()));
856 assert_eq!(mgr.last_error, None);
857 }
858
859 #[test]
862 fn set_last_error_round_trips_extreme_payloads() {
863 let huge = "x".repeat(1 << 16);
864 let unicode = String::from("\u{0}dénié 🛰️\u{0301}\u{fffd}中文\u{1f680}");
865 let payloads = [
866 LocationError { code: 0, message: String::new() },
867 LocationError { code: u32::MAX, message: huge.clone() },
868 LocationError { code: 1, message: unicode.clone() },
869 ];
870
871 for err in payloads {
872 let mut mgr = GeolocationManager::new();
873 mgr.set_last_error(err.clone());
874 let got = mgr.last_error.clone().expect("error was stored");
875 assert_eq!(got, err, "error payload did not round-trip");
876 assert_eq!(got.message.len(), err.message.len());
877
878 let events = mgr.get_pending_events(tick());
879 assert_eq!(events.len(), 1);
880 assert_eq!(events[0].event_type, EventType::GeolocationError);
881 }
882
883 assert_eq!(huge.len(), 1 << 16);
885 assert!(unicode.contains('\u{0}'));
886 }
887
888 #[test]
889 fn every_repeated_error_raises_its_own_event() {
890 let mut mgr = GeolocationManager::new();
891 let err = LocationError {
892 code: 7,
893 message: String::from("revoked"),
894 };
895
896 for _ in 0..5 {
897 mgr.set_last_error(err.clone());
898 let events = mgr.get_pending_events(tick());
899 assert_eq!(
900 events.len(),
901 1,
902 "a repeated error must still answer the live subscription"
903 );
904 assert_eq!(events[0].event_type, EventType::GeolocationError);
905 mgr.clear_pending_event();
906 assert!(mgr.get_pending_events(tick()).is_empty());
907 }
908 assert_eq!(mgr.last_error, Some(err));
909 }
910
911 #[test]
914 fn clear_pending_event_is_idempotent_and_spares_the_diff_queue() {
915 let mut mgr = GeolocationManager::new();
916 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
917 mgr.set_latest_fix(full_fix());
918 mgr.set_last_error(LocationError {
919 code: 1,
920 message: String::from("e"),
921 });
922
923 for _ in 0..3 {
924 mgr.clear_pending_event();
925 assert!(
926 mgr.get_pending_events(tick()).is_empty(),
927 "clearing twice must stay cleared"
928 );
929 }
930
931 let diffs = mgr.take_pending_events();
934 assert_eq!(diffs.len(), 1);
935 assert!(matches!(diffs[0], GeolocationDiffEvent::Subscribe { .. }));
936 assert!(mgr.latest_fix().is_some());
938 assert!(mgr.last_error.is_some());
939 }
940
941 #[test]
942 fn take_pending_events_drains_in_arrival_order_and_leaves_the_queue_empty() {
943 let mut mgr = GeolocationManager::new();
944 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0))); mgr.diff_layout(|emit| emit(probe_cfg(true, 0.0))); mgr.diff_layout(|_emit| {}); let events = mgr.take_pending_events();
949 assert_eq!(events.len(), 3);
950 assert!(matches!(events[0], GeolocationDiffEvent::Subscribe { .. }));
951 assert!(matches!(events[1], GeolocationDiffEvent::Reconfigure { .. }));
952 assert_eq!(events[2], GeolocationDiffEvent::Release);
953
954 assert!(mgr.take_pending_events().is_empty(), "taken, not copied");
955 assert!(mgr.take_pending_events().is_empty(), "still empty");
956 }
957
958 #[test]
959 fn get_pending_events_does_not_consume_the_flags() {
960 let mut mgr = GeolocationManager::new();
961 mgr.set_latest_fix(full_fix());
962 mgr.set_last_error(LocationError {
963 code: 3,
964 message: String::from("boom"),
965 });
966
967 for _ in 0..3 {
969 let events = mgr.get_pending_events(tick());
970 assert_eq!(events.len(), 2, "the getter is a read, not a drain");
971 assert_eq!(events[0].event_type, EventType::GeolocationFix);
972 assert_eq!(events[1].event_type, EventType::GeolocationError);
973 assert_eq!(events[0].target, DomNodeId::ROOT);
974 assert_eq!(events[1].target, DomNodeId::ROOT);
975 }
976
977 mgr.clear_pending_event();
978 assert!(mgr.get_pending_events(tick()).is_empty());
979 }
980
981 #[test]
984 fn no_probes_on_a_fresh_manager_emits_nothing() {
985 let mut mgr = GeolocationManager::new();
986 for _ in 0..4 {
987 mgr.diff_layout(|_emit| {});
988 assert_eq!(mgr.refcount(), 0);
989 assert!(!mgr.has_active_subscription());
990 assert_eq!(mgr.active_config, None);
991 assert!(
992 mgr.take_pending_events().is_empty(),
993 "0 → 0 must not emit a spurious Release"
994 );
995 }
996 }
997
998 #[test]
999 fn refcount_tracks_the_probe_count_and_gates_the_subscription_flag() {
1000 let mut mgr = GeolocationManager::new();
1001
1002 mgr.diff_layout(|emit| {
1003 for _ in 0..10_000 {
1004 emit(probe_cfg(false, 0.0));
1005 }
1006 });
1007 assert_eq!(mgr.refcount(), 10_000);
1008 assert!(mgr.has_active_subscription());
1009 assert_eq!(mgr.take_pending_events().len(), 1, "one Subscribe, not 10k");
1010
1011 mgr.diff_layout(|emit| {
1013 for _ in 0..3 {
1014 emit(probe_cfg(false, 0.0));
1015 }
1016 });
1017 assert_eq!(mgr.refcount(), 3);
1018 assert!(mgr.has_active_subscription());
1019 assert!(mgr.take_pending_events().is_empty());
1020
1021 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
1023 assert_eq!(mgr.refcount(), 1);
1024 assert!(mgr.has_active_subscription());
1025
1026 mgr.diff_layout(|_emit| {});
1027 assert_eq!(mgr.refcount(), 0);
1028 assert!(!mgr.has_active_subscription(), "0 probes ⇒ no subscription");
1029 assert_eq!(mgr.take_pending_events(), vec![GeolocationDiffEvent::Release]);
1030 }
1031
1032 #[test]
1033 fn the_first_probes_config_wins_the_subscribe() {
1034 let mut mgr = GeolocationManager::new();
1035 mgr.diff_layout(|emit| {
1036 emit(probe_cfg(true, 25.0));
1037 emit(probe_cfg(false, 999.0)); emit(probe_cfg(false, 0.0));
1039 });
1040 assert_eq!(mgr.refcount(), 3);
1041
1042 let events = mgr.take_pending_events();
1043 assert_eq!(events.len(), 1);
1044 match events[0] {
1045 GeolocationDiffEvent::Subscribe { config } => {
1046 assert!(config.high_accuracy);
1047 assert_eq!(config.max_accuracy_m, 25.0);
1048 }
1049 ref other => panic!("expected Subscribe, got {other:?}"),
1050 }
1051 assert_eq!(mgr.active_config, Some(probe_cfg(true, 25.0)));
1052
1053 mgr.diff_layout(|emit| {
1056 emit(probe_cfg(true, 25.0));
1057 emit(probe_cfg(false, 0.0));
1058 });
1059 assert!(mgr.take_pending_events().is_empty());
1060 }
1061
1062 #[test]
1063 fn release_clears_the_fix_and_a_resubscribe_starts_from_scratch() {
1064 let mut mgr = GeolocationManager::new();
1065 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
1066 assert!(mgr.set_latest_fix(full_fix()));
1067 assert!(mgr.take_pending_events().len() == 1);
1068
1069 mgr.diff_layout(|_emit| {}); assert_eq!(mgr.latest_fix(), None, "Release drops the stale fix");
1071 assert_eq!(mgr.active_config, None);
1072 assert_eq!(mgr.take_pending_events(), vec![GeolocationDiffEvent::Release]);
1073
1074 mgr.diff_layout(|emit| emit(probe_cfg(true, 5.0)));
1077 assert_eq!(mgr.refcount(), 1);
1078 assert_eq!(mgr.latest_fix(), None);
1079 let events = mgr.take_pending_events();
1080 assert_eq!(events.len(), 1);
1081 assert_eq!(
1082 events[0],
1083 GeolocationDiffEvent::Subscribe {
1084 config: probe_cfg(true, 5.0)
1085 }
1086 );
1087 }
1088
1089 #[test]
1090 fn release_leaves_the_fix_event_flag_raised_with_no_fix_behind_it() {
1091 let mut mgr = GeolocationManager::new();
1097 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
1098 assert!(mgr.set_latest_fix(full_fix()));
1099
1100 mgr.diff_layout(|_emit| {}); assert_eq!(mgr.latest_fix(), None);
1102
1103 let events = mgr.get_pending_events(tick());
1104 assert_eq!(events.len(), 1);
1105 assert_eq!(events[0].event_type, EventType::GeolocationFix);
1106
1107 mgr.clear_pending_event();
1108 assert!(mgr.get_pending_events(tick()).is_empty());
1109 }
1110
1111 #[test]
1112 fn a_nan_probe_config_settles_like_any_other() {
1113 let nan_cfg = probe_cfg(false, f32::NAN);
1119
1120 let mut mgr = GeolocationManager::new();
1121 mgr.diff_layout(|emit| emit(nan_cfg));
1122 assert_eq!(mgr.take_pending_events().len(), 1, "the initial Subscribe");
1123
1124 for _ in 0..3 {
1125 mgr.diff_layout(|emit| emit(nan_cfg));
1126 assert!(
1127 mgr.take_pending_events().is_empty(),
1128 "an unchanged NaN config must NOT re-queue a Reconfigure"
1129 );
1130 }
1131
1132 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
1134 assert_eq!(mgr.take_pending_events().len(), 1, "one Reconfigure to sane");
1135 mgr.diff_layout(|emit| emit(probe_cfg(false, 0.0)));
1136 assert!(mgr.take_pending_events().is_empty(), "then quiet");
1137 }
1138
1139 #[test]
1140 fn extreme_probe_configs_survive_the_diff() {
1141 let extremes = [
1142 probe_cfg(true, f32::MAX),
1143 probe_cfg(false, f32::INFINITY),
1144 probe_cfg(true, -0.0),
1145 GeolocationProbeConfig {
1146 high_accuracy: true,
1147 background: true,
1148 max_accuracy_m: f32::MIN_POSITIVE,
1149 min_interval_ms: u32::MAX,
1150 },
1151 ];
1152
1153 let mut mgr = GeolocationManager::new();
1154 for cfg in extremes {
1155 mgr.diff_layout(|emit| emit(cfg));
1156 assert_eq!(mgr.refcount(), 1);
1157 assert_eq!(mgr.active_config, Some(cfg));
1158 let events = mgr.take_pending_events();
1159 assert_eq!(events.len(), 1, "each distinct config emits exactly one event");
1160 match events[0] {
1161 GeolocationDiffEvent::Subscribe { config }
1162 | GeolocationDiffEvent::Reconfigure { config } => {
1163 assert_eq!(config.max_accuracy_m.to_bits(), cfg.max_accuracy_m.to_bits());
1164 assert_eq!(config.min_interval_ms, cfg.min_interval_ms);
1165 }
1166 GeolocationDiffEvent::Release => panic!("probes are mounted, not released"),
1167 }
1168 }
1169 }
1170
1171 #[test]
1174 fn manager_equality_is_bit_blind_for_nan_fixes() {
1175 let mut with_nan = GeolocationManager::new();
1180 with_nan.set_latest_fix(nan_fix());
1181 assert_ne!(with_nan, with_nan.clone());
1182
1183 let mut without_nan = GeolocationManager::new();
1184 without_nan.set_latest_fix(full_fix());
1185 assert_eq!(without_nan, without_nan.clone());
1186 }
1187}