use std::time::Duration;
use calloop::EventLoop;
use calloop::channel::Event as ChannelEvent;
use tablero::notifications::notifications_from_swaync;
use tablero::producer::{ProducerBridge, from_fn};
use tablero::render::Bounds;
use tablero::widget::{
ClickButton, Command, Dashboard, Msg, Notifications, NotificationsWidget, Widget,
};
struct Harness {
dashboard: Dashboard,
seen: usize,
redraws: usize,
}
impl Harness {
fn new() -> Self {
Self {
dashboard: Dashboard::new(vec![Box::new(NotificationsWidget::new(Bounds::new(
0, 0, 320, 32,
)))]),
seen: 0,
redraws: 0,
}
}
}
fn pump_until(
event_loop: &mut EventLoop<Harness>,
harness: &mut Harness,
done: impl Fn(&Harness) -> bool,
) {
let mut waited = Duration::ZERO;
let step = Duration::from_millis(20);
while !done(harness) && waited < Duration::from_secs(5) {
event_loop
.dispatch(step, harness)
.expect("loop dispatch succeeds");
waited += step;
}
}
fn drive(readings: Vec<Msg>) -> usize {
let mut harness = Harness::new();
let mut event_loop: EventLoop<Harness> = EventLoop::try_new().expect("event loop");
let handle = event_loop.handle();
let (bridge, channel) = ProducerBridge::new().expect("runtime starts");
let expected = readings.len();
handle
.insert_source(channel, |event, _, h: &mut Harness| {
if let ChannelEvent::Msg(msg) = event {
if h.dashboard.update(&msg) {
h.redraws += 1;
}
if matches!(msg, Msg::Notifications(_)) {
h.seen += 1;
}
}
})
.expect("channel registers");
bridge.spawn(from_fn("fake-notifications", move |tx| async move {
for msg in readings {
tx.send(msg)?;
}
Ok(())
}));
pump_until(&mut event_loop, &mut harness, |h| h.seen >= expected);
drop(bridge);
harness.redraws
}
fn reading(count: u32, dnd: bool) -> Msg {
Msg::Notifications(Some(notifications_from_swaync(count, dnd)))
}
#[test]
fn common_notification_states_each_force_a_redraw() {
let redraws = drive(vec![
reading(0, false),
reading(1, false),
reading(1, true),
Msg::Notifications(None),
reading(0, false),
]);
assert_eq!(redraws, 5, "every reading changes the visible state");
}
#[test]
fn identical_reading_is_not_a_redraw() {
let redraws = drive(vec![reading(2, false), reading(2, false)]);
assert_eq!(redraws, 1, "no-op state changes must not repaint");
}
#[test]
fn readings_differing_only_in_dropped_daemon_fields_are_no_ops() {
let a = Msg::Notifications(Some(notifications_from_swaync(3, false)));
let b = Msg::Notifications(Some(Notifications::new(3, false)));
let redraws = drive(vec![a, b]);
assert_eq!(redraws, 1);
}
#[test]
fn absent_reading_before_any_notification_is_not_a_redraw() {
let redraws = drive(vec![Msg::Notifications(None)]);
assert_eq!(redraws, 0);
}
#[test]
fn clicks_route_by_button_through_the_dashboard() {
let mut dashboard = Dashboard::new(vec![Box::new(NotificationsWidget::new(Bounds::new(
0, 0, 320, 32,
)))]);
dashboard.update(&reading(1, false));
assert_eq!(
dashboard.on_click(10, 10, ClickButton::Left),
Some(Command::ToggleNotificationPanel)
);
assert_eq!(
dashboard.on_click(10, 10, ClickButton::Right),
Some(Command::ToggleNotificationsDnd)
);
}
#[test]
fn clicks_are_ignored_while_the_daemon_is_absent() {
let dashboard = Dashboard::new(vec![Box::new(NotificationsWidget::new(Bounds::new(
0, 0, 320, 32,
)))]);
assert_eq!(dashboard.on_click(10, 10, ClickButton::Left), None);
assert_eq!(dashboard.on_click(10, 10, ClickButton::Right), None);
}
#[test]
fn the_widget_reserves_no_slot_before_any_reading() {
let widget = NotificationsWidget::new(Bounds::new(0, 0, 320, 32));
let mut ctx = tablero::render::RenderContext::new(320, 32);
assert_eq!(widget.measure(&mut ctx, 32), 0);
}