use crabshell_reactive::Runtime;
use super::{Harness, capture_logs, paths};
use crate::{
Anchors, Button, Cx, Edge, Icon, KeyboardFocus, Layer, Margins, Output, PanelWindow, Region, Row,
Spacer, Stack, Style, SurfaceConfig, Text, Window,
};
#[test]
fn every_widget_gets_a_path_for_log_lines() {
let mut h = Harness::new();
let row = Row::new()
.child(Text::new("a"))
.child(Spacer::new())
.child(Button::new(Stack::new().child(Icon::new("x"))));
let window = h.window(h.panel().child(row), 200.0, 30.0);
assert_eq!(
paths(&window),
[
"PanelWindow[DP-1]",
"PanelWindow[DP-1]/Row",
"PanelWindow[DP-1]/Row/Text[0]",
"PanelWindow[DP-1]/Row/Spacer[1]",
"PanelWindow[DP-1]/Row/Button[2]",
"PanelWindow[DP-1]/Row/Button[2]/Stack",
"PanelWindow[DP-1]/Row/Button[2]/Stack/Icon[0]",
]
);
}
#[test]
fn a_panicking_binding_is_logged_with_its_widget_path() {
let mut h = Harness::new();
let fail = h.cx.signal(false);
let label = h.cx.signal(String::from("fine"));
let row = Row::new()
.child(Text::new(move || label.get()))
.child(Button::new(Text::new(move || {
assert!(!fail.get(), "binding failure");
String::from("ok")
})));
let window = h.window(h.panel().child(row), 200.0, 30.0);
let logs = capture_logs(|| {
fail.set(true);
h.frame(&window);
});
assert!(
logs.contains("effect PanelWindow[DP-1]/Row/Button[1]/Text.text panicked and is disabled"),
"{logs}"
);
label.set(String::from("still fine"));
fail.set(false);
h.frame(&window);
let view = window.view();
let text = |path: &[usize]| match super::node_at(view.root(), path).content() {
crate::Content::Text(text) => text.content().to_owned(),
_ => panic!("not text"),
};
assert_eq!(text(&[0, 0]), "still fine");
assert_eq!(text(&[0, 1, 0]), "ok");
}
#[test]
fn dropping_a_window_frees_everything_it_created() {
let mut h = Harness::new();
let count = h.cx.signal(0);
let items = h.cx.signal(vec![1, 2]);
let before = h.runtime.node_count();
let row = Row::new()
.gap(move || count.get())
.child(
Button::new(Text::new(move || count.get().to_string()))
.style(move || Style::new().radius(count.get() as f32))
.on_click(move |_| count.update(|n| *n += 1)),
)
.children_keyed(
move || items.get(),
|n| *n,
|cx, n| {
let local = cx.signal(1);
Text::new(move || format!("{} {}", n.get(), local.get()))
},
);
let window = h.window(
h.panel().height(move || 30 + count.get()).child(row),
200.0,
30.0,
);
assert!(h.runtime.node_count() > before);
drop(window);
assert_eq!(h.runtime.node_count(), before);
count.set(5);
h.runtime.flush();
assert_eq!(count.get(), 5);
}
#[test]
fn surface_settings_have_layer_shell_defaults() {
let mut h = Harness::new();
let window = h.window(h.panel(), 200.0, 30.0);
let surface = window.surface();
assert_eq!(surface.anchors, Anchors::NONE);
assert_eq!((surface.width, surface.height), (0, 0));
assert_eq!(surface.exclusive_zone, 0);
assert_eq!(surface.layer, Layer::Top);
assert_eq!(surface.margins, Margins::default());
assert_eq!(surface.keyboard_focus, KeyboardFocus::None);
assert_eq!(surface.namespace, "crabshell");
assert_eq!(surface.mask, None);
assert_eq!(surface, SurfaceConfig::default());
assert_eq!(window.output(), Output::new("DP-1"));
}
#[test]
fn margins_focus_namespace_and_mask_reach_the_surface_settings() {
let mut h = Harness::new();
let launcher = h.cx.signal(false);
let panel = h
.panel()
.anchors(Edge::Top | Edge::Left | Edge::Right)
.height(30)
.margins(Margins::new(8, 4, 0, 4))
.keyboard_focus(move || {
if launcher.get() {
KeyboardFocus::Exclusive
} else {
KeyboardFocus::OnDemand
}
})
.namespace("bar")
.mask(move || (!launcher.get()).then(|| Region::rect(0, 0, 40, 30)));
let window = h.window(panel, 200.0, 30.0);
let surface = window.surface();
assert_eq!(surface.margins, Margins::new(8, 4, 0, 4));
assert_eq!(surface.keyboard_focus, KeyboardFocus::OnDemand);
assert_eq!(surface.namespace, "bar");
assert_eq!(surface.mask, Some(Region::rect(0, 0, 40, 30)));
assert!(window.take_surface_changed());
launcher.set(true);
h.frame(&window);
assert!(window.take_surface_changed());
assert_eq!(window.surface().keyboard_focus, KeyboardFocus::Exclusive);
assert_eq!(window.surface().mask, None);
}
#[test]
fn the_default_zone_leaves_out_the_margin() {
let mut h = Harness::new();
let panel = PanelWindow::new(&Output::new("DP-1"))
.anchors(Edge::Top | Edge::Left | Edge::Right)
.height(30)
.margins(Margins::new(8, 4, 0, 4));
assert_eq!(h.window(panel, 200.0, 30.0).surface().exclusive_zone, 30);
}
#[test]
fn the_exclusive_zone_defaults_to_the_bar_thickness() {
let surface = |panel: PanelWindow| {
let mut h = Harness::new();
h.window(panel, 200.0, 30.0).surface()
};
let output = Output::new("DP-1");
let top = PanelWindow::new(&output)
.anchors(Edge::Top | Edge::Left | Edge::Right)
.height(30);
assert_eq!(surface(top).exclusive_zone, 30);
let left = PanelWindow::new(&output)
.anchors(Edge::Left)
.width(40)
.height(500);
assert_eq!(surface(left).exclusive_zone, 40);
let corner = PanelWindow::new(&output)
.anchors(Edge::Top | Edge::Left)
.height(30);
assert_eq!(surface(corner).exclusive_zone, 0);
let explicit = PanelWindow::new(&output)
.anchors(Edge::Bottom)
.height(30)
.exclusive_zone(-1)
.layer(Layer::Overlay);
let explicit = surface(explicit);
assert_eq!(explicit.exclusive_zone, -1);
assert_eq!(explicit.layer, Layer::Overlay);
}
#[test]
fn anchors_name_the_edge_a_bar_sits_on() {
assert_eq!(Anchors::from(Edge::Top).edge(), Some(Edge::Top));
assert_eq!(
(Edge::Bottom | Edge::Left | Edge::Right).edge(),
Some(Edge::Bottom)
);
assert_eq!(
(Edge::Top | Edge::Bottom | Edge::Left).edge(),
Some(Edge::Left)
);
assert_eq!(
(Edge::Top | Edge::Bottom | Edge::Right).edge(),
Some(Edge::Right)
);
assert_eq!((Edge::Top | Edge::Bottom).edge(), None);
assert_eq!(
(Edge::Top | Edge::Bottom | Edge::Left | Edge::Right).edge(),
None
);
assert_eq!(Anchors::NONE.edge(), None);
assert!((Edge::Top | Edge::Left).contains(Edge::Left));
assert!(!(Edge::Top | Edge::Left).contains(Edge::Right));
}
#[test]
fn surface_bindings_report_a_change() {
let h = Harness::new();
let tall = h.cx.signal(false);
let panel = h
.panel()
.anchors(Edge::Top)
.height(move || if tall.get() { 40 } else { 30 });
let window = Window::new(&h.cx, panel);
h.runtime.flush();
assert!(window.take_surface_changed());
assert!(!window.take_surface_changed());
assert_eq!(window.surface().height, 30);
tall.set(true);
h.runtime.flush();
assert!(window.take_surface_changed());
assert_eq!(window.surface().height, 40);
assert_eq!(window.surface().exclusive_zone, 40);
tall.set(true);
h.runtime.flush();
assert!(!window.take_surface_changed());
}
#[test]
fn bindings_first_run_at_the_next_flush() {
let h = Harness::new();
let window = Window::new(
&h.cx,
h.panel().child(Text::new(move || String::from("bound"))),
);
let text = |window: &Window| match super::node_at(window.view().root(), &[0]).content() {
crate::Content::Text(text) => text.content().to_owned(),
_ => panic!("not text"),
};
assert_eq!(text(&window), "");
h.runtime.flush();
assert_eq!(text(&window), "bound");
}
struct Flushes;
impl crate::Widget for Flushes {
fn into_element(self: Box<Self>) -> crate::Element {
crabshell_reactive::Runtime::current().flush();
Box::new(Spacer::new()).into_element()
}
}
#[test]
fn descriptions_run_before_the_window_is_built() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("x"));
let row = Row::new()
.child(Text::new(move || label.get()))
.child(Button::new(Flushes));
let window = h.window(h.panel().child(row), 200.0, 30.0);
let view = window.view();
let crate::Content::Text(text) = super::node_at(view.root(), &[0, 0]).content() else {
panic!("a text node has text content");
};
assert_eq!(text.content(), "x");
}
#[test]
fn an_icon_name_can_be_a_string() {
let mut h = Harness::new();
let name = String::from("audio-volume-high");
let window = h.window(h.panel().child(Icon::new(name)), 100.0, 30.0);
let view = window.view();
let crate::Content::Icon(icon) = super::node_at(view.root(), &[0]).content() else {
panic!("an icon node has icon content");
};
assert_eq!(
icon.source,
&crate::IconSource::Name(String::from("audio-volume-high"))
);
}
#[test]
fn a_child_context_belongs_to_its_parent() {
let root = Cx::new(Runtime::current().new_owner());
let parent = root.child();
let child = parent.child().named("child");
let value = child.signal(1);
parent.owner().dispose();
assert_eq!(value.try_get(), None);
}
struct ReadsSignal(crabshell_reactive::Signal<u32>);
impl crate::Widget for ReadsSignal {
fn into_element(self: Box<Self>) -> crate::Element {
Box::new(Icon::new("x").size(self.0.get())).into_element()
}
}
#[test]
fn a_window_built_in_an_effect_adds_no_dependency_to_it() {
let h = Harness::new();
let size = h.cx.signal(4_u32);
let runs = super::Counter::default();
let counted = runs.clone();
let cx = h.cx.clone();
let windows = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let kept = std::rc::Rc::clone(&windows);
h.cx.effect(move || {
counted.hit();
let panel = PanelWindow::new(&Output::new("DP-1")).child(ReadsSignal(size));
kept.borrow_mut().push(Window::new(&cx, panel));
});
h.runtime.flush();
assert_eq!(runs.get(), 1);
size.set(5);
h.runtime.flush();
assert_eq!(runs.get(), 1);
}
#[test]
fn a_blur_region_reaches_the_window_and_a_change_is_reported() {
let mut h = Harness::new();
let frosted = h.cx.signal(true);
let panel = h.panel().blur(move || {
frosted
.get()
.then(|| Region::rect(0, 0, 200, 30).radius(8.0))
});
let window = h.window(panel, 200.0, 30.0);
assert_eq!(window.blur(), Some(Region::rect(0, 0, 200, 30).radius(8.0)));
assert!(window.take_surface_changed());
frosted.set(false);
h.frame(&window);
assert!(window.take_surface_changed());
assert_eq!(window.blur(), None);
assert_eq!(
h.window(h.panel(), 200.0, 30.0).blur(),
None,
"none by default"
);
}
#[test]
fn a_panicking_blur_is_logged_with_its_path() {
let mut h = Harness::new();
let logs = super::capture_logs(|| {
let panel = h.panel().blur(|| -> Option<Region> { panic!("no frost") });
let window = h.window(panel, 200.0, 30.0);
assert_eq!(window.blur(), None);
});
assert!(
logs.contains("PanelWindow[DP-1].blur panicked and is disabled"),
"{logs}"
);
}
#[test]
fn inhibiting_idle_reaches_the_window_and_a_change_is_reported() {
let mut h = Harness::new();
let awake = h.cx.signal(true);
let window = h.window(h.panel().inhibit_idle(move || awake.get()), 200.0, 30.0);
assert!(window.inhibits_idle());
assert!(window.take_surface_changed());
awake.set(false);
h.frame(&window);
assert!(window.take_surface_changed());
assert!(!window.inhibits_idle());
assert!(
!h.window(h.panel(), 200.0, 30.0).inhibits_idle(),
"off by default"
);
}
#[test]
fn a_panicking_inhibit_idle_is_logged_with_its_path() {
let mut h = Harness::new();
let logs = super::capture_logs(|| {
let panel = h.panel().inhibit_idle(|| -> bool { panic!("no sleep") });
let window = h.window(panel, 200.0, 30.0);
assert!(!window.inhibits_idle());
});
assert!(
logs.contains("PanelWindow[DP-1].inhibit_idle panicked and is disabled"),
"{logs}"
);
}
#[test]
fn inhibiting_shortcuts_reaches_the_window_and_a_change_is_reported() {
let mut h = Harness::new();
let typing = h.cx.signal(true);
let window = h.window(
h.panel().inhibit_shortcuts(move || typing.get()),
200.0,
30.0,
);
assert!(window.inhibits_shortcuts());
assert!(window.take_surface_changed());
typing.set(false);
h.frame(&window);
assert!(window.take_surface_changed());
assert!(!window.inhibits_shortcuts());
assert!(
!h.window(h.panel(), 200.0, 30.0).inhibits_shortcuts(),
"off by default"
);
}
#[test]
fn a_panicking_inhibit_shortcuts_is_logged_with_its_path() {
let mut h = Harness::new();
let logs = super::capture_logs(|| {
let panel = h
.panel()
.inhibit_shortcuts(|| -> bool { panic!("no keys") });
let window = h.window(panel, 200.0, 30.0);
assert!(!window.inhibits_shortcuts());
});
assert!(
logs.contains("PanelWindow[DP-1].inhibit_shortcuts panicked and is disabled"),
"{logs}"
);
}
#[test]
fn the_signals_the_compositor_writes_reach_the_window() {
let mut h = Harness::new();
let inhibited = h.cx.signal(false);
let grabbed = h.cx.signal(true);
let window = h.window(
h.panel().shortcuts_inhibited(inhibited).focus_grab(grabbed),
200.0,
30.0,
);
assert_eq!(window.shortcuts_inhibited(), Some(inhibited));
assert_eq!(window.focus_grab(), Some(grabbed));
let plain = h.window(h.panel(), 200.0, 30.0);
assert_eq!(
(plain.shortcuts_inhibited(), plain.focus_grab()),
(None, None)
);
}