use super::super::*;
use super::support::*;
use crate::nav::transition::{PageTransition, Timing, TransitionSpec};
use crate::test_support::RecordingScene;
use frust_core::Curve;
use frust_core::{FrameTime, RenderRoot, any};
use std::cell::Cell;
use std::time::Duration;
#[test]
fn compute_back_interest_covers_depth_and_policy() {
assert!(!compute_back_interest(1, BackPolicy::Pop));
assert!(compute_back_interest(2, BackPolicy::Pop));
assert!(compute_back_interest(1, BackPolicy::Veto));
assert!(compute_back_interest(1, BackPolicy::DismissAnimated));
}
#[test]
fn back_interest_is_vetoed_at_read_time_by_an_unreachable_host_page() {
let controller: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| navigator(&ctrl, || sized_page(10.0, 10.0))
};
let mut state = ();
root.rebuild(&mut app, &mut state);
controller.push(|| sized_page(20.0, 20.0));
root.rebuild(&mut app, &mut state);
assert!(controller.back_interest(), "top level -> back interest");
let reach = Rc::new(Cell::new(false));
controller.bind_host_reach(Some(Rc::clone(&reach)));
assert!(
!controller.back_interest(),
"an unreachable host page vetoes the press"
);
assert!(
controller.back_interest.get(),
"...while the PUBLISHED cell still reports the stack's own answer — \
the reach gate is ANDed on at read time, never baked into the cell"
);
reach.set(true);
assert!(controller.back_interest(), "revealed -> interest returns");
controller
.back_interest
.set(compute_back_interest(1, BackPolicy::Veto));
assert!(
controller.back_interest(),
"a reachable Veto overlay claims"
);
reach.set(false);
assert!(
!controller.back_interest(),
"a Veto overlay on an unreachable page claims nothing either"
);
controller.bind_host_reach(None);
assert!(controller.back_interest(), "no hosting page -> no gate");
}
#[test]
fn back_interest_publishes_through_the_controller() {
let controller: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| navigator(&ctrl, || sized_page(10.0, 10.0))
};
let mut state = ();
root.rebuild(&mut app, &mut state);
assert!(!controller.back_interest(), "root only -> no back interest");
controller.push(|| sized_page(20.0, 20.0));
root.rebuild(&mut app, &mut state);
assert!(controller.back_interest(), "depth 2 -> back interest");
controller.pop();
root.rebuild(&mut app, &mut state);
controller.push_with_options(
|| sized_page(30.0, 30.0),
PushOptions::transparent().back(BackPolicy::Veto),
);
root.rebuild(&mut app, &mut state);
assert!(
controller.back_interest(),
"a Veto overlay claims back interest"
);
}
struct NestedHarness {
outer: NavigatorController<()>,
nested: NavigatorController<()>,
root: RenderRoot<(), NavigatorView<()>>,
app: AppLogic,
}
impl NestedHarness {
fn new(cull: bool) -> Self {
let outer: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
let app: AppLogic = {
let outer = outer.clone();
let nested = nested.clone();
Box::new(move |_: &mut ()| {
let nested = nested.clone();
navigator(&outer, move || {
any(navigator(&nested, || sized_page(10.0, 10.0)))
})
.cull_covered_builds(cull)
})
};
Self {
outer,
nested,
root: RenderRoot::new(),
app,
}
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
}
#[test]
fn a_nested_navigator_loses_back_interest_when_its_page_is_covered() {
for cull in [false, true] {
let mut h = NestedHarness::new(cull);
let (outer, nested) = (h.outer.clone(), h.nested.clone());
h.rebuild();
assert!(!outer.back_interest(), "outer at its root (cull={cull})");
assert!(!nested.back_interest(), "nested at its root (cull={cull})");
nested.push(|| sized_page(20.0, 20.0));
h.rebuild();
assert!(
nested.back_interest(),
"a nested navigator on the CURRENT page claims back (cull={cull})"
);
outer.push(|| sized_page(30.0, 30.0));
h.rebuild();
assert_eq!((outer.depth(), nested.depth()), (2, 2));
assert!(
!nested.back_interest(),
"a nested navigator on a COVERED page claims nothing (cull={cull})"
);
assert!(
outer.back_interest(),
"the outer navigator claims it instead (cull={cull})"
);
for _ in 0..3 {
h.rebuild();
}
assert!(
!nested.back_interest(),
"and keeps claiming nothing while covered (cull={cull})"
);
outer.pop();
h.rebuild();
assert!(
nested.back_interest(),
"revealed: the nested navigator claims back again (cull={cull})"
);
}
}
#[test]
fn a_nested_navigator_under_a_transparent_overlay_reports_no_back_interest() {
let mut h = NestedHarness::new(false);
let (outer, nested) = (h.outer.clone(), h.nested.clone());
h.rebuild();
nested.push(|| sized_page(20.0, 20.0));
h.rebuild();
assert!(nested.back_interest());
outer.push_transparent(|| sized_page(30.0, 30.0));
h.rebuild();
assert!(
!nested.back_interest(),
"painted but not routed input: no back interest either"
);
}
#[test]
fn a_nested_navigator_on_a_page_leaving_in_a_pop_transition_claims_nothing() {
let outer: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let outer = outer.clone();
move |_: &mut ()| {
navigator(&outer, || sized_page(10.0, 10.0))
.transition(TransitionSpec::duration(PageTransition::IosPush))
}
};
let mut state = ();
root.rebuild(&mut app, &mut state);
{
let nested = nested.clone();
outer.push(move || {
let nested = nested.clone();
any(navigator(&nested, || sized_page(20.0, 20.0)))
});
}
root.rebuild(&mut app, &mut state);
nested.push(|| sized_page(30.0, 30.0));
root.rebuild(&mut app, &mut state);
assert!(nested.back_interest(), "current page: it claims back");
outer.pop();
root.rebuild(&mut app, &mut state);
assert!(
!nested.back_interest(),
"a navigator on the leaving page claims nothing mid-flight"
);
}
#[test]
fn back_reach_composes_through_three_nesting_levels() {
let outer: NavigatorController<()> = NavigatorController::new();
let middle: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let outer = outer.clone();
let middle = middle.clone();
let inner = inner.clone();
move |_: &mut ()| {
let middle = middle.clone();
let inner = inner.clone();
navigator(&outer, move || {
let inner = inner.clone();
any(navigator(&middle, move || {
any(navigator(&inner, || sized_page(10.0, 10.0)))
}))
})
}
};
let mut state = ();
root.rebuild(&mut app, &mut state);
inner.push(|| sized_page(20.0, 20.0));
root.rebuild(&mut app, &mut state);
assert!(inner.back_interest(), "the innermost navigator claims back");
outer.push(|| sized_page(30.0, 30.0));
root.rebuild(&mut app, &mut state);
assert!(
!inner.back_interest(),
"an ancestor two levels up covering its page silences the innermost"
);
assert!(!middle.back_interest());
assert!(outer.back_interest(), "the outermost claims it");
}
#[test]
fn request_back_pops_plain_page_preserving_transition() {
let controller: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| navigator(&ctrl, || sized_page(100.0, 100.0))
};
let mut state = ();
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
controller.push_with(
|| sized_page(100.0, 60.0),
TransitionSpec::new(
PageTransition::IosPush,
Timing::Duration(Duration::from_millis(100), Curve::Linear),
),
);
for t in [0u64, 50, 150, 300] {
full_frame(&mut root, &mut app, &mut state, ft(t));
}
assert_eq!(controller.depth(), 2, "B pushed");
controller.request_back();
let (f0, _) = full_frame(&mut root, &mut app, &mut state, ft(1000));
assert!(
f0.iter().any(|(_, s, _)| (s.height - 100.0).abs() < 1e-9),
"A paints during the pop"
);
assert!(
f0.iter().any(|(_, s, _)| (s.height - 60.0).abs() < 1e-9),
"B (leaving) still paints during the pop"
);
for t in [1050u64, 1150, 1300] {
full_frame(&mut root, &mut app, &mut state, ft(t));
}
assert_eq!(controller.depth(), 1, "request_back popped one page");
}
#[test]
fn request_back_at_root_is_a_noop() {
let controller: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| navigator(&ctrl, || sized_page(10.0, 10.0))
};
let mut state = ();
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 1);
controller.request_back();
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 1, "root back does not pop the root");
}
#[test]
fn request_back_dismiss_animated_fires_signal_once_no_pop() {
let controller: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| navigator(&ctrl, || sized_page(100.0, 100.0))
};
let mut state = ();
root.rebuild(&mut app, &mut state);
let signal = Rc::new(Cell::new(0u64));
controller.push_with_options(
|| sized_page(20.0, 20.0),
PushOptions::transparent()
.back(BackPolicy::DismissAnimated)
.dismiss_signal(signal.clone()),
);
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 2, "overlay pushed");
assert_eq!(signal.get(), 0, "no back yet");
controller.request_back();
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 2, "DismissAnimated does not pop");
assert_eq!(signal.get(), 1, "signal fired exactly once");
controller.request_back();
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 2);
assert_eq!(signal.get(), 2, "signal fired once per request");
}
#[test]
fn request_back_veto_consumes_without_change() {
let controller: NavigatorController<()> = NavigatorController::new();
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| navigator(&ctrl, || sized_page(100.0, 100.0))
};
let mut state = ();
root.rebuild(&mut app, &mut state);
let signal = Rc::new(Cell::new(0u64));
controller.push_with_options(
|| sized_page(20.0, 20.0),
PushOptions::transparent()
.back(BackPolicy::Veto)
.dismiss_signal(signal.clone()),
);
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 2);
controller.request_back();
root.rebuild(&mut app, &mut state);
assert_eq!(controller.depth(), 2, "Veto leaves the stack unchanged");
assert_eq!(signal.get(), 0, "Veto fires no dismiss signal");
}
#[test]
fn overlay_host_is_a_navigator_with_two_defaults_changed() {
let controller: NavigatorController<()> = NavigatorController::new();
let view = overlay_host(&controller, || sized_page(10.0, 10.0));
assert_eq!(
view.pop_swipe,
Some(false),
"the host pins pop_swipe off explicitly, not by default-derivation"
);
assert!(!view.resolve_pop_swipe());
assert_eq!(view.default_transition, TransitionSpec::NONE);
let (controller, mut root, mut app, ..) = overlay_host_fixture();
let mut state = ();
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
controller.push_with_options(|| sized_page(1e4, 1e4), PushOptions::transparent());
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
assert!(
!controller.transition().active,
"TransitionSpec::NONE: the overlay push settles instantly"
);
assert_eq!(controller.depth(), 2);
root.event(&mut state, &down(2.0, 50.0));
root.event(&mut state, &move_to(80.0, 50.0));
root.event(&mut state, &up(90.0, 50.0));
root.rebuild(&mut app, &mut state);
assert_eq!(
controller.depth(),
2,
"pop_swipe(false): an edge swipe never dismisses an overlay"
);
}
#[test]
fn an_overlay_takes_the_pointer_from_the_chrome_beneath_it() {
let (controller, mut root, mut app, content_hits, chrome_hits) = overlay_host_fixture();
let mut state = ();
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
root.event(&mut state, &down(50.0, 50.0));
assert_eq!(chrome_hits.get(), 1, "no overlay: the chrome takes presses");
assert_eq!(content_hits.get(), 0, "the chrome is above the content");
let overlay_hits = Rc::new(Cell::new(0u32));
controller.push_with_options(
{
let overlay_hits = overlay_hits.clone();
move || host_probe("overlay", &overlay_hits)
},
PushOptions::transparent(),
);
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
chrome_hits.set(0);
content_hits.set(0);
root.event(&mut state, &down(50.0, 50.0));
assert_eq!(overlay_hits.get(), 1, "the overlay takes the press");
assert_eq!(
chrome_hits.get(),
0,
"the chrome under a root overlay is inert — the whole point of #44"
);
assert_eq!(content_hits.get(), 0, "so is the app content");
}
#[test]
fn an_overlays_scrim_rect_equals_the_window_rect() {
let (controller, mut root, mut app, ..) = overlay_host_fixture();
let mut state = ();
let window = Size::new(320.0, 640.0);
root.rebuild(&mut app, &mut state);
root.layout(window);
controller.push_with_options(|| sized_page(1e4, 1e4), PushOptions::transparent());
root.rebuild(&mut app, &mut state);
root.layout(window);
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
assert!(
scene.rects.contains(&(Point::ZERO, window)),
"the overlay's scrim covers the whole window, not a sub-rect \
(recorded rects: {:?})",
scene.rects
);
}