1mod arbitrate;
9mod content;
10mod graph;
11mod layout;
12mod manager;
13mod model;
14mod presentation;
15mod switcher;
16
17pub use arbitrate::{Decision, OpenOutcome, Policy, arbitrate, normalize_initial_url};
18pub use content::{SlotKind, SurfaceContent};
19pub use graph::SurfaceGraph;
20pub use layout::PlanAsideSlot;
21pub use layout::{
22 Axis, BottomOwner, DEFAULT_HYSTERESIS, DerivedLayout, LayoutPresentationPlan, LayoutTree,
23 PlanAside, PlanFloat, SizeClass, SplitForm, SwitcherForm,
24};
25pub use manager::SurfaceManager;
26pub use model::{
27 Edge, FloatAnchor, FloatDismiss, FloatSpec, Placement, Role, Surface, SurfaceId,
28 SurfaceInteraction, SurfaceOwner, SurfaceState,
29};
30pub use presentation::{SurfaceCapabilities, SurfaceIcon, SurfacePresentation};
31pub use switcher::{
32 CloseOutcome, ReplaceMainsError, SurfaceSwitcherItem, SurfaceSwitcherSnapshot,
33 SwitcherContentKind,
34};
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39
40 fn main_s(id: &str) -> Surface {
41 Surface::lxapp(id, Role::Main, id)
42 }
43 fn aside_s(id: &str, edge: Edge) -> Surface {
44 let mut s = Surface::lxapp(id, Role::Aside, id);
45 s.placement.edge = Some(edge);
46 s
47 }
48 fn web_aside_s(id: &str, url: &str, edge: Edge) -> Surface {
49 let mut s = aside_s(id, edge);
50 s.content = SurfaceContent::Browser {
51 initial_url: url.to_string(),
52 reuse_by_url: true,
53 };
54 s
55 }
56
57 fn non_reusable_web_aside_s(id: &str, url: &str, edge: Edge) -> Surface {
58 let mut surface = web_aside_s(id, url, edge);
59 if let SurfaceContent::Browser { reuse_by_url, .. } = &mut surface.content {
60 *reuse_by_url = false;
61 }
62 surface
63 }
64 fn terminal_aside_s(id: &str, edge: Edge) -> Surface {
65 let mut s = Surface::native(id, Role::Aside, "terminal");
66 s.placement.edge = Some(edge);
67 s
68 }
69
70 #[test]
73 fn empty_graph_is_valid() {
74 let g = SurfaceGraph::new();
75 assert!(g.is_valid());
76 assert_eq!(g.active_main_id, None);
77 assert_eq!(g.focused_surface_id, None);
78 }
79
80 #[test]
81 fn first_main_becomes_active_and_focused() {
82 let mut g = SurfaceGraph::new();
83 g.insert(main_s("home"));
84 assert_eq!(g.active_main_id.as_deref(), Some("home"));
85 assert_eq!(g.focused_surface_id.as_deref(), Some("home"));
86 assert!(g.is_valid());
87 }
88
89 #[test]
90 fn root_main_keeps_companion_graph_anchored() {
91 let mut g = SurfaceGraph::new();
93 g.insert(main_s("home"));
94 g.insert(aside_s("assistant", Edge::Right));
95 assert!(g.is_valid());
96 assert_eq!(
99 g.close("home"),
100 CloseOutcome::RejectedRoot {
101 surface_id: "home".into()
102 }
103 );
104 assert_eq!(g.asides().len(), 1);
105 assert_eq!(g.active_main_id.as_deref(), Some("home"));
106 assert!(g.is_valid());
107 }
108
109 #[test]
110 fn closing_active_main_picks_adjacent_successor() {
111 let mut g = SurfaceGraph::new();
112 g.insert(main_s("a"));
113 g.insert(main_s("b"));
114 g.insert(main_s("c"));
115 g.set_active_main("b");
116 g.close("b");
117 assert_eq!(g.active_main_id.as_deref(), Some("c"));
119 assert!(g.is_valid());
120 }
121
122 #[test]
123 fn modal_float_restores_focus_on_close() {
124 let mut g = SurfaceGraph::new();
125 g.insert(main_s("home"));
126 assert_eq!(g.focused_surface_id.as_deref(), Some("home"));
127 let mut modal = Surface::native("dialog", Role::Float, "confirm");
128 modal.float = Some(FloatSpec {
129 modal: true,
130 ..Default::default()
131 });
132 g.insert(modal);
133 g.set_focus("dialog");
134 g.close("dialog");
135 assert_eq!(g.focused_surface_id.as_deref(), Some("home"));
136 assert!(g.is_valid());
137 }
138
139 #[test]
142 fn single_main_no_switcher_no_split() {
143 let mut g = SurfaceGraph::new();
144 g.insert(main_s("home"));
145 let d = g.derive_layout(SizeClass::Expanded);
146 assert_eq!(d.switcher_form, SwitcherForm::None);
147 assert_eq!(d.split_form, SplitForm::None);
148 assert!(matches!(d.layout_tree, Some(LayoutTree::Leaf { .. })));
149 }
150
151 #[test]
152 fn switcher_only_with_multiple_mains() {
153 let mut g = SurfaceGraph::new();
154 g.insert(main_s("a"));
155 assert_eq!(
156 g.derive_layout(SizeClass::Expanded).switcher_form,
157 SwitcherForm::None
158 );
159 g.insert(main_s("b"));
160 assert_eq!(
161 g.derive_layout(SizeClass::Expanded).switcher_form,
162 SwitcherForm::Sidebar
163 );
164 }
165
166 #[test]
167 fn aside_splits_on_expanded_fullscreen_on_compact() {
168 let mut g = SurfaceGraph::new();
169 g.insert(main_s("home"));
170 g.insert(aside_s("assistant", Edge::Right));
171 assert_eq!(
172 g.derive_layout(SizeClass::Expanded).split_form,
173 SplitForm::Split
174 );
175 assert_eq!(
176 g.derive_layout(SizeClass::Compact).split_form,
177 SplitForm::FullScreen
178 );
179 }
180
181 #[test]
182 fn compact_plan_keeps_existing_aside_desired() {
183 let mut g = SurfaceGraph::new();
184 g.insert(main_s("home"));
185 g.insert(aside_s("assistant", Edge::Right));
186
187 let plan = g.presentation_plan(
188 SizeClass::Compact,
189 390.0,
190 &crate::arbitrate::Policy::default(),
191 );
192 assert_eq!(plan.split_form, SplitForm::FullScreen);
193 assert!(plan.asides.iter().any(|aside| aside.id == "assistant"));
194 assert!(
195 plan.tree
196 .as_ref()
197 .is_some_and(|tree| tree.surface_ids().iter().any(|id| id == "assistant"))
198 );
199 }
200
201 #[test]
202 fn compact_bottom_owner_stays_app() {
203 let mut g = SurfaceGraph::new();
204 g.insert(main_s("a"));
205 assert_eq!(
207 g.derive_layout(SizeClass::Compact).bottom_owner,
208 BottomOwner::App
209 );
210 g.insert(main_s("b"));
211 assert_eq!(
213 g.derive_layout(SizeClass::Compact).bottom_owner,
214 BottomOwner::App
215 );
216 }
217
218 #[test]
219 fn canonical_layout_validates() {
220 let mut g = SurfaceGraph::new();
221 g.insert(main_s("a"));
222 g.insert(main_s("b"));
223 g.insert(aside_s("assistant", Edge::Right));
224 let tree = g.canonical_layout(SizeClass::Expanded).unwrap();
225 tree.validate().expect("canonical tree must be valid");
226 g.insert(Surface::native("toast", Role::Float, "toast"));
228 let ids = g
229 .canonical_layout(SizeClass::Expanded)
230 .unwrap()
231 .surface_ids();
232 assert!(!ids.contains(&"toast".to_string()));
233 }
234
235 #[test]
238 fn breakpoints_align_to_material() {
239 assert_eq!(SizeClass::from_width(599.0), SizeClass::Compact);
240 assert_eq!(SizeClass::from_width(600.0), SizeClass::Medium);
241 assert_eq!(SizeClass::from_width(840.0), SizeClass::Medium);
242 assert_eq!(SizeClass::from_width(841.0), SizeClass::Expanded);
243 }
244
245 #[test]
246 fn hysteresis_holds_class_near_boundary() {
247 let held = SizeClass::resolve(Some(SizeClass::Medium), 590.0, DEFAULT_HYSTERESIS);
249 assert_eq!(held, SizeClass::Medium);
250 let switched = SizeClass::resolve(Some(SizeClass::Medium), 500.0, DEFAULT_HYSTERESIS);
252 assert_eq!(switched, SizeClass::Compact);
253 }
254
255 #[test]
256 fn hysteresis_does_not_hold_across_two_classes() {
257 assert_eq!(
258 SizeClass::resolve(Some(SizeClass::Expanded), 590.0, DEFAULT_HYSTERESIS),
259 SizeClass::Compact
260 );
261 assert_eq!(
262 SizeClass::resolve(Some(SizeClass::Compact), 850.0, DEFAULT_HYSTERESIS),
263 SizeClass::Expanded
264 );
265 }
266
267 #[test]
270 fn same_kind_asides_join_their_slot() {
271 let mut g = SurfaceGraph::new();
272 g.insert(main_s("home"));
273 g.insert(aside_s("a1", Edge::Right));
274 let (next, decision) = arbitrate(
277 &g,
278 aside_s("a2", Edge::Right),
279 &Policy::default(),
280 SizeClass::Expanded,
281 );
282 assert_eq!(decision, Decision::MergedIntoTabs);
283 assert!(next.get("a1").is_some());
284 assert!(next.get("a2").is_some());
285 let slots = next.aside_slots(SizeClass::Expanded);
286 assert_eq!(slots.len(), 1);
287 assert_eq!(slots[0].kind, SlotKind::Lxapp);
288 assert_eq!(slots[0].children, vec!["a1".to_string(), "a2".to_string()]);
289 assert_eq!(slots[0].active_child.as_deref(), Some("a2"));
290 assert!(slots[0].visible);
291 assert!(next.is_valid());
292 }
293
294 #[test]
295 fn slots_hide_beyond_admission_never_evict() {
296 let mut next = SurfaceGraph::new();
297 next.insert(main_s("home"));
298 for request in [
299 aside_s("chat", Edge::Right),
300 terminal_aside_s("terminal", Edge::Bottom),
301 web_aside_s("b1", "https://a.example", Edge::Right),
302 ] {
303 let (n, d) = arbitrate(&next, request, &Policy::default(), SizeClass::Expanded);
304 assert_eq!(d, Decision::Accepted);
305 next = n;
306 }
307 let expanded = next.aside_slots(SizeClass::Expanded);
309 assert_eq!(expanded.len(), 3);
310 assert!(expanded.iter().all(|slot| slot.visible));
311 let medium = next.aside_slots(SizeClass::Medium);
314 assert_eq!(medium.iter().filter(|slot| slot.visible).count(), 1);
315 assert!(
316 medium
317 .iter()
318 .find(|slot| slot.visible)
319 .is_some_and(|slot| slot.kind == SlotKind::Browser)
320 );
321 assert_eq!(next.asides().len(), 3);
322 assert!(next.is_valid());
323 }
324
325 #[test]
326 fn physical_admission_caps_below_the_count_ceiling() {
327 let mut next = SurfaceGraph::new();
329 next.insert(main_s("home"));
330 for request in [
331 aside_s("chat", Edge::Right),
332 web_aside_s("b1", "https://a.example", Edge::Right),
333 terminal_aside_s("terminal", Edge::Bottom),
334 ] {
335 let (n, _) = arbitrate(&next, request, &Policy::default(), SizeClass::Expanded);
336 next = n;
337 }
338 let policy = Policy::default(); let wide = next.aside_slots_admitted(SizeClass::Expanded, 1200.0, &policy);
343 assert_eq!(wide.iter().filter(|s| s.visible).count(), 3);
344
345 let narrow = next.aside_slots_admitted(SizeClass::Expanded, 700.0, &policy);
349 let visible_right = narrow
350 .iter()
351 .filter(|s| s.visible && !matches!(s.edge, Some(Edge::Bottom)))
352 .count();
353 assert_eq!(visible_right, 1, "only one right slot fits at 700pt");
354 assert!(
356 narrow
357 .iter()
358 .any(|s| s.visible && matches!(s.edge, Some(Edge::Bottom)))
359 );
360 assert_eq!(next.asides().len(), 3);
362 }
363
364 #[test]
365 fn slot_admission_uses_policy_and_true_focus_recency() {
366 let mut graph = SurfaceGraph::new();
367 graph.insert(main_s("home"));
368 graph.insert(aside_s("chat", Edge::Right));
369 graph.insert(web_aside_s("browser", "https://example.com", Edge::Right));
370
371 let medium = graph.aside_slots(SizeClass::Medium);
373 assert_eq!(
374 medium
375 .iter()
376 .find(|slot| slot.visible)
377 .map(|slot| slot.kind),
378 Some(SlotKind::Browser)
379 );
380
381 assert!(graph.set_focus("chat"));
383 let medium = graph.aside_slots(SizeClass::Medium);
384 assert_eq!(
385 medium
386 .iter()
387 .find(|slot| slot.visible)
388 .map(|slot| slot.kind),
389 Some(SlotKind::Lxapp)
390 );
391
392 let policy = Policy {
393 max_asides_expanded: 1,
394 ..Policy::default()
395 };
396 let expanded = graph.aside_slots_admitted(SizeClass::Expanded, 1200.0, &policy);
397 assert_eq!(expanded.iter().filter(|slot| slot.visible).count(), 1);
398 assert_eq!(
399 expanded
400 .iter()
401 .find(|slot| slot.visible)
402 .map(|slot| slot.kind),
403 Some(SlotKind::Lxapp)
404 );
405 }
406
407 #[test]
408 fn physical_admission_keeps_the_most_recent_horizontal_slot() {
409 let mut graph = SurfaceGraph::new();
410 graph.insert(main_s("home"));
411 graph.insert(aside_s("chat", Edge::Right));
412 graph.insert(web_aside_s("browser", "https://example.com", Edge::Right));
413 graph.insert(terminal_aside_s("terminal", Edge::Right));
414
415 let admitted = graph.aside_slots_admitted(SizeClass::Expanded, 700.0, &Policy::default());
418 let visible: Vec<_> = admitted
419 .iter()
420 .filter(|slot| slot.visible)
421 .map(|slot| slot.kind)
422 .collect();
423 assert_eq!(visible, vec![SlotKind::Native]);
424 }
425
426 #[test]
427 fn physical_admission_falls_back_to_an_older_fitting_slot() {
428 let mut graph = SurfaceGraph::new();
429 graph.insert(main_s("home"));
430 let mut terminal = terminal_aside_s("terminal", Edge::Top);
431 terminal.placement.edge = Some(Edge::Top);
432 graph.insert(terminal);
433 graph.insert(aside_s("chat", Edge::Right));
434
435 let admitted = graph.aside_slots_admitted(SizeClass::Medium, 500.0, &Policy::default());
439 let visible: Vec<_> = admitted
440 .iter()
441 .filter(|slot| slot.visible)
442 .map(|slot| slot.kind)
443 .collect();
444 assert_eq!(visible, vec![SlotKind::Native]);
445 }
446
447 #[test]
448 fn web_asides_coexist_as_tabs() {
449 let mut g = SurfaceGraph::new();
450 g.insert(main_s("home"));
451 g.insert(web_aside_s("browser-1", "https://a.example", Edge::Right));
452 let (next, decision) = arbitrate(
456 &g,
457 web_aside_s("browser-2", "https://b.example", Edge::Right),
458 &Policy::default(),
459 SizeClass::Expanded,
460 );
461 assert_eq!(decision, Decision::MergedIntoTabs);
462 assert_eq!(decision.resolved_surface_id, "browser-2");
463 assert!(next.get("browser-1").is_some());
464 assert!(next.get("browser-2").is_some());
465 assert_eq!(
466 next.asides()
467 .iter()
468 .filter(|s| matches!(s.content, SurfaceContent::Browser { .. }))
469 .count(),
470 2
471 );
472 assert!(next.is_valid());
473 }
474
475 #[test]
476 fn hiding_active_aside_selects_recent_visible_sibling() {
477 let mut graph = SurfaceGraph::new();
478 graph.insert(main_s("home"));
479 graph.insert(aside_s("first", Edge::Right));
480 graph.insert(aside_s("second", Edge::Right));
481 graph.set_focus("first");
482 graph.set_focus("second");
483
484 assert!(graph.hide("second"));
485 assert_eq!(graph.focused_surface_id.as_deref(), Some("first"));
486 assert_eq!(
487 graph.get("second").map(|surface| surface.state),
488 Some(SurfaceState::Hidden)
489 );
490 let slots = graph.aside_slots(SizeClass::Expanded);
491 assert_eq!(slots[0].active_child.as_deref(), Some("first"));
492
493 assert!(graph.show("second"));
494 assert_eq!(graph.focused_surface_id.as_deref(), Some("second"));
495 assert_eq!(
496 graph.get("second").map(|surface| surface.state),
497 Some(SurfaceState::Mounted)
498 );
499 }
500
501 #[test]
502 fn closing_active_aside_selects_recent_visible_sibling() {
503 let mut graph = SurfaceGraph::new();
504 graph.insert(main_s("home"));
505 graph.insert(web_aside_s("first", "https://one.example", Edge::Right));
506 graph.insert(web_aside_s("second", "https://two.example", Edge::Right));
507 graph.set_focus("first");
508 graph.set_focus("second");
509
510 assert_eq!(
511 graph.close("second"),
512 CloseOutcome::Closed {
513 removed: vec!["second".into()]
514 }
515 );
516 assert_eq!(graph.focused_surface_id.as_deref(), Some("first"));
517 assert_eq!(
518 graph.aside_slots(SizeClass::Expanded)[0]
519 .active_child
520 .as_deref(),
521 Some("first")
522 );
523 }
524
525 #[test]
526 fn web_aside_dedups_by_url() {
527 let mut g = SurfaceGraph::new();
528 g.insert(main_s("home"));
529 g.insert(web_aside_s("browser-1", "https://a.example", Edge::Right));
530 let (next, decision) = arbitrate(
533 &g,
534 web_aside_s("browser-2", "https://a.example", Edge::Right),
535 &Policy::default(),
536 SizeClass::Expanded,
537 );
538 assert_eq!(decision, Decision::MergedIntoTabs);
539 assert_eq!(decision.resolved_surface_id, "browser-1");
540 assert_eq!(decision.resolved_role, Role::Aside);
541 assert!(next.get("browser-1").is_some());
542 assert!(next.get("browser-2").is_none());
543 assert_eq!(
544 next.asides()
545 .iter()
546 .filter(|s| matches!(s.content, SurfaceContent::Browser { .. }))
547 .count(),
548 1
549 );
550 assert!(next.is_valid());
551 }
552
553 #[test]
554 fn non_reusable_web_asides_never_dedup_by_url() {
555 let mut graph = SurfaceGraph::new();
556 graph.insert(main_s("home"));
557 graph.insert(web_aside_s("ordinary", "https://a.example", Edge::Right));
558
559 let (graph, callback_outcome) = arbitrate(
560 &graph,
561 non_reusable_web_aside_s("callback", "https://a.example", Edge::Right),
562 &Policy::default(),
563 SizeClass::Expanded,
564 );
565 assert_eq!(callback_outcome.resolved_surface_id, "callback");
566 assert!(graph.get("ordinary").is_some());
567 assert!(graph.get("callback").is_some());
568
569 let (graph, ordinary_outcome) = arbitrate(
570 &graph,
571 web_aside_s("ordinary-2", "https://a.example", Edge::Right),
572 &Policy::default(),
573 SizeClass::Expanded,
574 );
575 assert_eq!(ordinary_outcome.resolved_surface_id, "ordinary");
576 assert!(graph.get("ordinary-2").is_none());
577 assert!(graph.get("callback").is_some());
578 }
579
580 #[test]
581 fn web_aside_url_key_normalizes_origin_and_empty_path_only() {
582 assert_eq!(
583 normalize_initial_url("HTTPS://Example.COM:443"),
584 "https://example.com/"
585 );
586 assert_eq!(
587 normalize_initial_url("https://example.com/?q=One#Top"),
588 "https://example.com/?q=One#Top"
589 );
590 assert_ne!(
591 normalize_initial_url("https://example.com/?q=One#Top"),
592 normalize_initial_url("https://example.com/?q=one#Top")
593 );
594
595 let mut graph = SurfaceGraph::new();
596 graph.insert(main_s("home"));
597 let (graph, _) = arbitrate(
598 &graph,
599 web_aside_s("browser-1", "HTTPS://Example.COM:443", Edge::Right),
600 &Policy::default(),
601 SizeClass::Expanded,
602 );
603 let (graph, outcome) = arbitrate(
604 &graph,
605 web_aside_s("browser-2", "https://example.com/", Edge::Right),
606 &Policy::default(),
607 SizeClass::Expanded,
608 );
609 assert_eq!(outcome.resolved_surface_id, "browser-1");
610 assert!(graph.get("browser-2").is_none());
611 }
612
613 #[test]
614 fn browser_asides_never_evict_a_declared_aside() {
615 let mut next = SurfaceGraph::new();
616 next.insert(main_s("home"));
617 next.insert(aside_s("chat", Edge::Right)); for (i, url) in [
622 "https://a.example",
623 "https://b.example",
624 "https://c.example",
625 ]
626 .iter()
627 .enumerate()
628 {
629 let (n, d) = arbitrate(
630 &next,
631 web_aside_s(&format!("b{i}"), url, Edge::Right),
632 &Policy::default(),
633 SizeClass::Expanded,
634 );
635 let expected = if i == 0 {
637 Decision::Accepted
638 } else {
639 Decision::MergedIntoTabs
640 };
641 assert_eq!(d, expected);
642 next = n;
643 }
644 assert!(next.get("chat").is_some(), "declared aside must survive");
645 assert_eq!(
646 next.asides()
647 .iter()
648 .filter(|s| matches!(s.content, SurfaceContent::Browser { .. }))
649 .count(),
650 3
651 );
652 assert!(next.is_valid());
653 }
654
655 #[test]
656 fn web_aside_coexists_with_a_page_aside() {
657 let mut g = SurfaceGraph::new();
658 g.insert(main_s("home"));
659 g.insert(aside_s("assistant", Edge::Right));
660 let (next, decision) = arbitrate(
663 &g,
664 web_aside_s("browser-1", "https://a.example", Edge::Left),
665 &Policy::default(),
666 SizeClass::Expanded,
667 );
668 assert_eq!(decision, Decision::Accepted);
669 assert!(next.get("assistant").is_some());
670 assert!(next.get("browser-1").is_some());
671 assert_eq!(next.asides().len(), 2);
672 assert!(next.is_valid());
673 }
674
675 #[test]
676 fn aside_fullscreen_fallback_on_compact() {
677 let mut g = SurfaceGraph::new();
678 g.insert(main_s("home"));
679 let (next, decision) = arbitrate(
680 &g,
681 aside_s("assistant", Edge::Right),
682 &Policy::default(),
683 SizeClass::Compact,
684 );
685 assert_eq!(decision, Decision::FullScreenFallback);
686 assert!(decision.overlay);
687 assert_eq!(next.role_of("assistant"), Some(Role::Aside));
688 assert_eq!(next.active_main_id.as_deref(), Some("home"));
689 assert_eq!(
690 next.presentation_plan(
691 SizeClass::Compact,
692 390.0,
693 &crate::arbitrate::Policy::default()
694 )
695 .active_main_id
696 .as_deref(),
697 Some("home")
698 );
699 assert!(next.is_valid());
700 }
701
702 #[test]
703 fn aside_without_primary_promotes_to_main() {
704 let g = SurfaceGraph::new();
707 let (next, decision) = arbitrate(
708 &g,
709 aside_s("assistant", Edge::Right),
710 &Policy::default(),
711 SizeClass::Expanded,
712 );
713 assert_eq!(decision, Decision::DowngradedRole);
714 assert_eq!(next.role_of("assistant"), Some(Role::Main));
715 assert!(next.is_valid());
716 }
717
718 #[test]
719 fn arbitrate_is_pure_and_keeps_graph_valid() {
720 let mut g = SurfaceGraph::new();
721 g.insert(main_s("home"));
722 let before = g.surfaces().len();
723 let (next, _) = arbitrate(
724 &g,
725 aside_s("x", Edge::Left),
726 &Policy::default(),
727 SizeClass::Expanded,
728 );
729 assert_eq!(g.surfaces().len(), before);
731 assert!(next.is_valid());
732 }
733
734 #[test]
737 fn surface_json_round_trip() {
738 let s = aside_s("assistant", Edge::Right);
739 let json = serde_json::to_string(&s).unwrap();
740 let back: Surface = serde_json::from_str(&json).unwrap();
741 assert_eq!(s, back);
742 }
743
744 #[test]
745 fn browser_surface_json_preserves_reuse_policy_default() {
746 let ordinary = web_aside_s("ordinary", "https://a.example", Edge::Right);
747 let ordinary_json = serde_json::to_string(&ordinary).unwrap();
748 assert!(!ordinary_json.contains("reuseByUrl"));
749 assert_eq!(
750 serde_json::from_str::<Surface>(&ordinary_json).unwrap(),
751 ordinary
752 );
753
754 let callback = non_reusable_web_aside_s("callback", "https://a.example", Edge::Right);
755 let callback_json = serde_json::to_string(&callback).unwrap();
756 assert!(callback_json.contains("\"reuseByUrl\":false"));
757 assert_eq!(
758 serde_json::from_str::<Surface>(&callback_json).unwrap(),
759 callback
760 );
761 }
762}