use std::cell::RefCell;
use std::rc::Rc;
use frust_reactive::{CanPopRegistration, back_presses, set_can_pop_provider, set_handles_back};
use frust_widgets::{NavigatorController, NavigatorId};
use reactive_graph::traits::{Get, GetUntracked};
thread_local! {
static SHARED: RefCell<SharedBack> = const { RefCell::new(SharedBack::new()) };
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum Role {
Host,
Navigator,
}
struct Registrant {
id: NavigatorId,
role: Role,
birth: u64,
seq: u64,
mounted: Box<dyn Fn() -> bool>,
interest: Box<dyn Fn() -> bool>,
request_back: Rc<dyn Fn()>,
}
impl Registrant {
fn key(&self) -> (Role, u64) {
match self.role {
Role::Host => (self.role, self.birth),
Role::Navigator => (self.role, u64::MAX - self.birth),
}
}
}
struct SharedBack {
consumed: Option<u64>,
registrants: Vec<Registrant>,
seq: u64,
provider: Option<CanPopRegistration>,
}
impl SharedBack {
const fn new() -> Self {
Self {
consumed: None,
registrants: Vec::new(),
seq: 0,
provider: None,
}
}
}
fn any_interest() -> bool {
SHARED.with(|shared| shared.borrow().registrants.iter().any(|r| (r.interest)()))
}
fn refresh_interest<State: 'static>(controller: &NavigatorController<State>, role: Role) {
let id = controller.id();
let mounted_probe = controller.clone();
let interest_probe = controller.clone();
let request_probe = controller.clone();
SHARED.with(|shared| {
let mut shared = shared.borrow_mut();
shared.seq += 1;
let seq = shared.seq;
let (role, birth) = match shared.registrants.iter().position(|r| r.id == id) {
Some(index) => {
let previous = shared.registrants[index].seq;
let carried = (
shared.registrants[index].role.min(role),
shared.registrants[index].birth,
);
shared
.registrants
.retain(|r| r.id == id || r.seq >= previous || (r.mounted)());
carried
}
None => (role, seq),
};
let entry = Registrant {
id,
role,
birth,
seq,
mounted: Box::new(move || mounted_probe.is_mounted()),
interest: Box::new(move || interest_probe.back_interest()),
request_back: Rc::new(move || request_probe.request_back()),
};
shared.registrants.retain(|r| r.id != id);
let at = shared
.registrants
.iter()
.position(|r| r.key() > entry.key())
.unwrap_or(shared.registrants.len());
shared.registrants.insert(at, entry);
if shared.provider.is_none() {
shared.provider = Some(set_can_pop_provider(Box::new(any_interest)));
}
});
set_handles_back(any_interest());
}
fn route_back<State: 'static>(fallback: &NavigatorController<State>) {
let claimant = SHARED.with(|shared| {
let shared = shared.borrow();
shared
.registrants
.iter()
.find(|r| (r.interest)())
.map(|r| Rc::clone(&r.request_back))
});
match claimant {
Some(request_back) => request_back(),
None => fallback.request_back(),
}
}
fn consume_back<State: 'static>(controller: &NavigatorController<State>) {
let count = back_presses().count.get();
let fire = SHARED.with(|shared| {
let mut shared = shared.borrow_mut();
match shared.consumed {
None => {
shared.consumed = Some(count);
false
}
Some(prev) if prev == count => false,
Some(_) => {
shared.consumed = Some(count);
true
}
}
});
if fire {
route_back(controller);
}
}
fn seed_consumed() {
let count = back_presses().count.get_untracked();
SHARED.with(|shared| {
let mut shared = shared.borrow_mut();
if shared.consumed.is_none() {
shared.consumed = Some(count);
}
});
}
pub(crate) fn auto_wire<State: 'static>(controller: &NavigatorController<State>) {
consume_back(controller);
refresh_interest(controller, Role::Navigator);
}
pub(crate) fn auto_wire_overlay_host<State: 'static>(controller: &NavigatorController<State>) {
consume_back(controller);
refresh_interest(controller, Role::Host);
}
pub struct BackHandler<State: 'static> {
controller: NavigatorController<State>,
}
impl<State: 'static> BackHandler<State> {
pub fn new(controller: NavigatorController<State>) -> Self {
seed_consumed();
refresh_interest(&controller, Role::Navigator);
Self { controller }
}
pub fn track(&self) {
consume_back(&self.controller);
refresh_interest(&self.controller, Role::Navigator);
}
pub fn controller(&self) -> &NavigatorController<State> {
&self.controller
}
}
pub fn attach_back_handler<State: 'static>(
controller: NavigatorController<State>,
) -> BackHandler<State> {
BackHandler::new(controller)
}
#[cfg(test)]
mod tests {
use super::*;
use frust_core::{AnyView, RenderRoot, any};
use frust_reactive::{
ReactiveRuntime, clear_can_pop_provider, handles_back, push_back_press, set_handles_back,
};
use frust_widgets::{
BackPolicy, NavigatorController, NavigatorView, PushOptions, navigator as raw_navigator,
overlay_host as raw_overlay_host,
};
use std::sync::{Arc, Mutex};
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn reset() {
clear_can_pop_provider();
set_handles_back(false);
SHARED.with(|shared| *shared.borrow_mut() = SharedBack::new());
}
fn page() -> AnyView<()> {
any(frust_widgets::text("x"))
}
type AppLogic = Box<dyn FnMut(&mut ()) -> NavigatorView<()>>;
struct Harness {
controller: NavigatorController<()>,
root: RenderRoot<(), NavigatorView<()>>,
app: AppLogic,
wire: Box<dyn FnMut()>,
}
impl Harness {
fn new(wire: impl FnMut(&NavigatorController<()>) + 'static) -> Self {
Self::with_controller(NavigatorController::new(), wire)
}
fn with_controller(
controller: NavigatorController<()>,
mut wire: impl FnMut(&NavigatorController<()>) + 'static,
) -> Self {
let app_controller = controller.clone();
let app: AppLogic = Box::new(move |_: &mut ()| raw_navigator(&app_controller, page));
let wire_controller = controller.clone();
let wire: Box<dyn FnMut()> = Box::new(move || wire(&wire_controller));
let mut harness = Harness {
controller,
root: RenderRoot::new(),
app,
wire,
};
harness.rebuild();
harness
}
fn rebuild(&mut self) {
(self.wire)();
self.root.rebuild(&mut self.app, &mut ());
}
fn depth(&self) -> usize {
self.controller.depth()
}
}
#[test]
fn auto_wire_pops_without_a_backhandler() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = Harness::new(auto_wire);
assert_eq!(h.depth(), 1, "starts at the root page");
assert!(!handles_back(), "depth 1 + no overlay: handles_back false");
h.controller.push(page);
h.rebuild();
assert_eq!(h.depth(), 2, "pushed to depth 2");
assert!(
handles_back(),
"poppable stack: handles_back true, same rebuild"
);
push_back_press();
h.rebuild();
assert_eq!(h.depth(), 1, "auto-wired back press pops one page");
assert!(
!handles_back(),
"back at the root: handles_back false again"
);
}
#[test]
fn veto_overlay_claims_back_but_does_not_pop() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = Harness::new(auto_wire);
h.controller
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Veto));
h.rebuild();
assert!(handles_back(), "a Veto overlay claims the back press");
let before = h.depth();
push_back_press();
h.rebuild();
assert_eq!(
h.depth(),
before,
"Veto consumes the press via request_back: stack unchanged (not popped)"
);
assert!(handles_back(), "still claiming back (overlay still on top)");
}
#[test]
fn explicit_backhandler_pops_and_tracks_handles_back() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let controller: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(controller.clone());
let mut h = Harness::with_controller(controller, move |_c| back.track());
assert_eq!(h.depth(), 1, "starts at the root");
assert!(
!handles_back(),
"root-level back bubbles: handles_back false"
);
h.controller.push(page);
h.rebuild();
assert_eq!(h.depth(), 2, "push published to depth immediately");
assert!(
handles_back(),
"poppable stack reports handles_back true on the SAME rebuild, no settle frame"
);
push_back_press();
h.rebuild();
assert_eq!(h.depth(), 1, "one back press pops one page");
assert!(
!handles_back(),
"root again: handles_back false immediately"
);
push_back_press();
h.rebuild();
assert_eq!(h.depth(), 1, "pop-at-root is a no-op");
clear_can_pop_provider();
assert!(
!handles_back(),
"after clear, handles_back reads the polled fallback"
);
}
#[test]
fn manual_backhandler_plus_auto_wire_pops_once() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let controller: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(controller.clone());
let mut h = Harness::with_controller(controller, move |c| {
back.track();
auto_wire(c);
});
h.controller.push(page);
h.rebuild();
h.controller.push(page);
h.rebuild();
assert_eq!(h.depth(), 3, "pushed to depth 3");
push_back_press();
h.rebuild();
assert_eq!(
h.depth(),
2,
"manual + auto share one consumption source: one press pops once"
);
h.rebuild();
assert_eq!(h.depth(), 2, "no new press -> no extra pop");
}
struct HostHarness {
host: NavigatorController<()>,
inner: NavigatorController<()>,
root: RenderRoot<(), NavigatorView<()>>,
app: AppLogic,
}
impl HostHarness {
fn new() -> Self {
let host: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let app: AppLogic = {
let host = host.clone();
let inner = inner.clone();
Box::new(move |_: &mut ()| {
auto_wire_overlay_host(&host);
let inner = inner.clone();
raw_overlay_host(&host, move || {
auto_wire(&inner);
any(raw_navigator(&inner, page))
})
})
};
let mut harness = Self {
host,
inner,
root: RenderRoot::new(),
app,
};
harness.rebuild();
harness
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
}
fn registrant_ids() -> Vec<frust_widgets::NavigatorId> {
SHARED.with(|shared| shared.borrow().registrants.iter().map(|r| r.id).collect())
}
#[test]
fn a_root_overlay_takes_the_back_press_from_the_inner_navigator() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = HostHarness::new();
assert_eq!(
registrant_ids(),
vec![h.host.id(), h.inner.id()],
"outermost first"
);
h.inner.push(page);
h.rebuild();
h.host
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!((h.host.depth(), h.inner.depth()), (2, 2));
assert!(handles_back(), "some registrant claims the press");
push_back_press();
h.rebuild();
assert_eq!(h.host.depth(), 1, "the press went to the root overlay host");
assert_eq!(
h.inner.depth(),
2,
"and NOT to the inner navigator (the root-modal back bug)"
);
}
#[test]
fn a_host_with_no_overlay_defers_the_back_press_to_the_inner_navigator() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = HostHarness::new();
assert!(
!handles_back(),
"two navigators, both at the root: back bubbles to the platform"
);
h.inner.push(page);
h.rebuild();
assert_eq!(h.host.depth(), 1, "the host stays empty");
assert!(
handles_back(),
"the inner navigator's interest answers for the whole app"
);
push_back_press();
h.rebuild();
assert_eq!(h.inner.depth(), 1, "the press popped the inner navigator");
assert_eq!(h.host.depth(), 1, "the empty host was untouched");
assert!(!handles_back(), "back at the root of both: bubbles again");
push_back_press();
h.rebuild();
assert_eq!((h.host.depth(), h.inner.depth()), (1, 1));
}
#[test]
fn a_root_overlay_claims_back_even_with_the_inner_navigator_at_its_root() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = HostHarness::new();
h.host
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!((h.host.depth(), h.inner.depth()), (2, 1));
assert!(
handles_back(),
"the root overlay claims the press though the inner navigator cannot pop"
);
push_back_press();
h.rebuild();
assert_eq!(h.host.depth(), 1, "the press dismissed the root overlay");
assert!(!handles_back(), "nothing left to claim it");
}
#[test]
fn a_navigator_that_stops_wiring_is_pruned_after_one_full_cycle() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
refresh_interest(&outer, Role::Navigator);
refresh_interest(&inner, Role::Navigator);
refresh_interest(&nested, Role::Navigator);
assert_eq!(
registrant_ids(),
vec![nested.id(), inner.id(), outer.id()],
"innermost/last-wired first: the reverse of build order"
);
refresh_interest(&outer, Role::Navigator);
refresh_interest(&inner, Role::Navigator);
assert_eq!(
registrant_ids(),
vec![nested.id(), inner.id(), outer.id()],
"not yet: a full cycle has not elapsed without it"
);
refresh_interest(&outer, Role::Navigator);
assert_eq!(
registrant_ids(),
vec![inner.id(), outer.id()],
"the departed navigator is pruned, and its controller clone released"
);
refresh_interest(&inner, Role::Navigator);
refresh_interest(&outer, Role::Navigator);
refresh_interest(&inner, Role::Navigator);
assert_eq!(registrant_ids(), vec![inner.id(), outer.id()]);
}
#[test]
fn an_overlay_host_survives_an_inner_navigator_wiring_twice_per_pass() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let host: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let back = Rc::new(BackHandler::new(inner.clone()));
let mut app: AppLogic = {
let host = host.clone();
let inner = inner.clone();
Box::new(move |_: &mut ()| {
auto_wire_overlay_host(&host);
let inner = inner.clone();
let back = Rc::clone(&back);
raw_overlay_host(&host, move || {
back.track();
auto_wire(&inner);
any(raw_navigator(&inner, page))
})
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert_eq!(
registrant_ids(),
vec![host.id(), inner.id()],
"the host outranks the inner navigator despite wiring once to its twice"
);
inner.push(page);
host.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
for _ in 0..2 {
root.rebuild(&mut app, &mut ());
}
assert_eq!((host.depth(), inner.depth()), (2, 2));
assert_eq!(
registrant_ids(),
vec![host.id(), inner.id()],
"still outermost-first after repeated passes"
);
assert!(
handles_back(),
"the shell is told the app handles back (a root modal is open)"
);
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(host.depth(), 1, "the press dismissed the root overlay");
assert_eq!(
inner.depth(),
2,
"and NOT the page under it (the root-modal back bug)"
);
}
#[test]
fn an_init_registered_backhandler_does_not_outrank_a_later_overlay_host() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let host: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(inner.clone());
assert_eq!(registrant_ids(), vec![inner.id()], "init wired it alone");
auto_wire_overlay_host(&host);
assert_eq!(
registrant_ids(),
vec![host.id(), inner.id()],
"the host claims the outermost slot even though it wired second"
);
auto_wire_overlay_host(&host);
back.track();
assert_eq!(registrant_ids(), vec![host.id(), inner.id()]);
}
#[test]
fn a_torn_down_navigator_stops_vetoing_the_prune_and_is_released() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
let show_nested = Rc::new(std::cell::Cell::new(true));
let mid_pass: Rc<RefCell<Vec<NavigatorId>>> = Rc::new(RefCell::new(Vec::new()));
let mut app: AppLogic = {
let outer = outer.clone();
let nested = nested.clone();
let show_nested = Rc::clone(&show_nested);
let mid_pass = Rc::clone(&mid_pass);
Box::new(move |_: &mut ()| {
auto_wire(&outer);
auto_wire(&outer);
let nested = nested.clone();
let show_nested = Rc::clone(&show_nested);
let mid_pass = Rc::clone(&mid_pass);
raw_navigator(&outer, move || {
*mid_pass.borrow_mut() = registrant_ids();
if show_nested.get() {
auto_wire(&nested);
any(raw_navigator(&nested, page))
} else {
page()
}
})
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert!(nested.is_mounted(), "the nested navigator is in the tree");
assert_eq!(registrant_ids(), vec![nested.id(), outer.id()]);
root.rebuild(&mut app, &mut ());
root.rebuild(&mut app, &mut ());
assert_eq!(
registrant_ids(),
vec![nested.id(), outer.id()],
"a mounted navigator is in the tree by definition: never pruned"
);
assert_eq!(
*mid_pass.borrow(),
vec![nested.id(), outer.id()],
"and it is never MISSING mid-pass either — the window in which a \
spurious prune would mis-answer handles_back / mis-route a press"
);
show_nested.set(false);
root.rebuild(&mut app, &mut ());
assert!(!nested.is_mounted(), "teardown dropped the liveness");
root.rebuild(&mut app, &mut ());
root.rebuild(&mut app, &mut ());
assert_eq!(
registrant_ids(),
vec![outer.id()],
"the departed navigator is released, not kept alive by the veto"
);
}
#[test]
fn a_nested_navigator_inside_a_pushed_page_gets_the_back_press() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let mut app: AppLogic = {
let outer_c = outer.clone();
Box::new(move |_: &mut ()| {
auto_wire(&outer_c);
raw_navigator(&outer_c, page)
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 1, "outer starts at its own root");
assert!(
!handles_back(),
"root-level, no nested navigator yet: back bubbles"
);
{
let inner_for_push = inner.clone();
outer.push(move || {
auto_wire(&inner_for_push);
any(raw_navigator(&inner_for_push, page))
});
}
root.rebuild(&mut app, &mut ());
assert_eq!(
outer.depth(),
2,
"outer pushed to depth 2: the page hosting the nested navigator"
);
assert_eq!(
inner.depth(),
1,
"the nested navigator starts at its own root"
);
inner.push(page);
root.rebuild(&mut app, &mut ());
assert_eq!((outer.depth(), inner.depth()), (2, 2));
assert!(handles_back(), "some registrant claims the press");
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(
inner.depth(),
1,
"the back press popped the NESTED navigator's stack"
);
assert_eq!(
outer.depth(),
2,
"and left the outer navigator's depth UNCHANGED — a bare \
build-order (outermost-first) arbitration would instead have \
popped the outer stack, discarding the whole page the nested \
navigator lives on"
);
}
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 ()| {
auto_wire(&outer);
let nested = nested.clone();
raw_navigator(&outer, move || {
auto_wire(&nested);
any(raw_navigator(&nested, page))
})
.cull_covered_builds(cull)
})
};
let mut harness = Self {
outer,
nested,
root: RenderRoot::new(),
app,
};
harness.rebuild();
harness
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
}
#[test]
fn a_nested_navigator_on_a_covered_page_does_not_take_the_back_press() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
for cull in [false, true] {
reset();
let mut h = NestedHarness::new(cull);
h.nested.push(page);
h.rebuild();
assert_eq!((h.outer.depth(), h.nested.depth()), (1, 2));
assert!(
handles_back(),
"the nested navigator claims the press while its page is current (cull={cull})"
);
h.outer.push(page);
h.rebuild();
assert_eq!((h.outer.depth(), h.nested.depth()), (2, 2));
for _ in 0..3 {
h.rebuild();
}
assert!(
handles_back(),
"the outer navigator claims it (cull={cull})"
);
push_back_press();
h.rebuild();
assert_eq!(
h.outer.depth(),
1,
"the press popped the OUTER stack — the page the user is \
actually looking at came off (cull={cull})"
);
assert_eq!(
h.nested.depth(),
2,
"and the covered navigator's invisible stack is UNCHANGED \
(cull={cull})"
);
push_back_press();
h.rebuild();
assert_eq!(
(h.outer.depth(), h.nested.depth()),
(1, 1),
"back reaches the nested navigator again once its page is \
current (cull={cull})"
);
}
}
#[test]
fn a_nested_navigator_under_a_transparent_overlay_does_not_take_the_back_press() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = NestedHarness::new(false);
h.nested.push(page);
h.rebuild();
h.outer
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!((h.outer.depth(), h.nested.depth()), (2, 2));
push_back_press();
h.rebuild();
assert_eq!(h.outer.depth(), 1, "the overlay came off");
assert_eq!(
h.nested.depth(),
2,
"the painted-but-inert page's navigator kept its stack"
);
}
#[test]
#[ignore = "pins a design case that FAILS against current ranking: an inner \
navigator whose explicit BackHandler wires (and keeps tracking) \
before its outer host's first view exists (the Component::init \
shape) loses the innermost-first tie-break to the outer \
navigator instead of winning it — Registrant::key ranks \
Role::Navigator peers by raw wire sequence (birth), not by \
structural nesting depth. Expected to start passing once \
ranking accounts for nesting depth rather than birth order \
alone; rerun with `cargo test -p frust-ui --lib \
wiring_order_does_not_flip -- --ignored` to observe the \
current failure."]
fn wiring_order_does_not_flip_the_innermost_navigator_when_it_is_still_poppable() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let inner_back = BackHandler::new(inner.clone());
assert_eq!(registrant_ids(), vec![inner.id()], "inner wired alone");
let mut app: AppLogic = {
let outer_c = outer.clone();
Box::new(move |_: &mut ()| {
inner_back.track();
auto_wire(&outer_c);
raw_navigator(&outer_c, page)
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 1, "outer starts at its own root");
{
let inner_for_push = inner.clone();
outer.push(move || {
auto_wire(&inner_for_push);
any(raw_navigator(&inner_for_push, page))
});
}
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 2, "outer pushed the page hosting inner");
inner.push(page);
root.rebuild(&mut app, &mut ());
assert_eq!((outer.depth(), inner.depth()), (2, 2));
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(
inner.depth(),
1,
"the structurally-innermost navigator should win the tie \
regardless of which one registered first"
);
assert_eq!(
outer.depth(),
2,
"and the outer stack should stay untouched"
);
}
#[test]
fn wiring_order_is_irrelevant_when_the_inner_navigator_has_no_interest() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let inner_back = BackHandler::new(inner.clone());
let mut app: AppLogic = {
let outer_c = outer.clone();
Box::new(move |_: &mut ()| {
inner_back.track();
auto_wire(&outer_c);
raw_navigator(&outer_c, page)
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
{
let inner_for_push = inner.clone();
outer.push(move || {
auto_wire(&inner_for_push);
any(raw_navigator(&inner_for_push, page))
});
}
root.rebuild(&mut app, &mut ());
assert_eq!(
(outer.depth(), inner.depth()),
(2, 1),
"outer poppable, inner still at its own root"
);
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 1, "outer popped its own shell page");
}
}