- React SPA: dashboard, data sources CRUD, widgets CRUD, layout builder, presets. TanStack Router + Query, shadcn/ui, Vite proxy to :3000 - wire media + rss adapters into polling loop, remove xtb source type - media adapter: read username/password from headers, proper subsonic auth - event handler: subscribe to LayoutChanged, push screen update to clients - fix clippy warnings across workspace (Default impls, collapsible ifs, redundant closures, is_none_or, unused imports)
167 lines
4.6 KiB
Rust
167 lines
4.6 KiB
Rust
use client_application::{ClientApp, RepaintCommand};
|
|
use client_domain::BoundingBox;
|
|
use protocol::{
|
|
ServerMessage, WidgetDescriptor, WireContainerNode, WireDirection, WireDisplayHint,
|
|
WireKeyValue, WireLayoutChild, WireLayoutNode, WireSizing, WireValue, WireWidgetState,
|
|
};
|
|
|
|
fn screen() -> BoundingBox {
|
|
BoundingBox::screen(240, 320)
|
|
}
|
|
|
|
fn weather_descriptor(id: u16, temp: &str) -> WidgetDescriptor {
|
|
WidgetDescriptor {
|
|
id,
|
|
display_hint: WireDisplayHint::IconValue,
|
|
state: WireWidgetState {
|
|
data: vec![WireKeyValue {
|
|
key: "temperature".into(),
|
|
value: WireValue::String(temp.into()),
|
|
}],
|
|
error: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn two_widget_layout() -> WireLayoutNode {
|
|
WireLayoutNode::Container(WireContainerNode {
|
|
direction: WireDirection::Row,
|
|
gap: 0,
|
|
padding: 0,
|
|
children: vec![
|
|
WireLayoutChild {
|
|
sizing: WireSizing::Flex(1),
|
|
node: WireLayoutNode::Leaf(1),
|
|
},
|
|
WireLayoutChild {
|
|
sizing: WireSizing::Flex(1),
|
|
node: WireLayoutNode::Leaf(2),
|
|
},
|
|
],
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn screen_update_repaints_all_widgets() {
|
|
let mut app = ClientApp::new(screen());
|
|
|
|
let msg = ServerMessage::ScreenUpdate {
|
|
layout: two_widget_layout(),
|
|
widgets: vec![
|
|
weather_descriptor(1, "5.4°C"),
|
|
weather_descriptor(2, "20°C"),
|
|
],
|
|
};
|
|
|
|
let repaints = app.handle_message(msg);
|
|
|
|
assert_eq!(repaints.len(), 2);
|
|
assert_eq!(repaints[0].widget_id, 1);
|
|
assert_eq!(repaints[0].bounds, BoundingBox::new(0, 0, 120, 320));
|
|
assert_eq!(repaints[1].widget_id, 2);
|
|
assert_eq!(repaints[1].bounds, BoundingBox::new(120, 0, 120, 320));
|
|
}
|
|
|
|
#[test]
|
|
fn data_update_only_repaints_changed_widgets() {
|
|
let mut app = ClientApp::new(screen());
|
|
|
|
app.handle_message(ServerMessage::ScreenUpdate {
|
|
layout: two_widget_layout(),
|
|
widgets: vec![
|
|
weather_descriptor(1, "5.4°C"),
|
|
weather_descriptor(2, "20°C"),
|
|
],
|
|
});
|
|
|
|
let repaints = app.handle_message(ServerMessage::DataUpdate {
|
|
widgets: vec![weather_descriptor(1, "6.1°C")],
|
|
});
|
|
|
|
assert_eq!(repaints.len(), 1);
|
|
assert_eq!(repaints[0].widget_id, 1);
|
|
assert_eq!(
|
|
repaints[0].state.data[0].value,
|
|
WireValue::String("6.1°C".into())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn data_update_with_unchanged_data_produces_no_repaints() {
|
|
let mut app = ClientApp::new(screen());
|
|
|
|
app.handle_message(ServerMessage::ScreenUpdate {
|
|
layout: two_widget_layout(),
|
|
widgets: vec![
|
|
weather_descriptor(1, "5.4°C"),
|
|
weather_descriptor(2, "20°C"),
|
|
],
|
|
});
|
|
|
|
let repaints = app.handle_message(ServerMessage::DataUpdate {
|
|
widgets: vec![weather_descriptor(1, "5.4°C")],
|
|
});
|
|
|
|
assert!(repaints.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn second_screen_update_repaints_all_widgets_with_new_layout() {
|
|
let mut app = ClientApp::new(screen());
|
|
|
|
app.handle_message(ServerMessage::ScreenUpdate {
|
|
layout: two_widget_layout(),
|
|
widgets: vec![
|
|
weather_descriptor(1, "5.4°C"),
|
|
weather_descriptor(2, "20°C"),
|
|
],
|
|
});
|
|
|
|
let column_layout = WireLayoutNode::Container(WireContainerNode {
|
|
direction: WireDirection::Column,
|
|
gap: 0,
|
|
padding: 0,
|
|
children: vec![
|
|
WireLayoutChild {
|
|
sizing: WireSizing::Flex(1),
|
|
node: WireLayoutNode::Leaf(1),
|
|
},
|
|
WireLayoutChild {
|
|
sizing: WireSizing::Flex(1),
|
|
node: WireLayoutNode::Leaf(2),
|
|
},
|
|
],
|
|
});
|
|
|
|
let repaints = app.handle_message(ServerMessage::ScreenUpdate {
|
|
layout: column_layout,
|
|
widgets: vec![
|
|
weather_descriptor(1, "5.4°C"),
|
|
weather_descriptor(2, "20°C"),
|
|
],
|
|
});
|
|
|
|
assert_eq!(repaints.len(), 2);
|
|
assert_eq!(repaints[0].bounds, BoundingBox::new(0, 0, 240, 160));
|
|
assert_eq!(repaints[1].bounds, BoundingBox::new(0, 160, 240, 160));
|
|
}
|
|
|
|
#[test]
|
|
fn data_update_before_screen_update_produces_no_repaints() {
|
|
let mut app = ClientApp::new(screen());
|
|
|
|
let repaints = app.handle_message(ServerMessage::DataUpdate {
|
|
widgets: vec![weather_descriptor(1, "5.4°C")],
|
|
});
|
|
|
|
assert!(repaints.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn heartbeat_produces_no_repaints() {
|
|
let mut app = ClientApp::new(screen());
|
|
|
|
let repaints = app.handle_message(ServerMessage::Heartbeat);
|
|
assert!(repaints.is_empty());
|
|
}
|