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