Server: domain (entities, value objects, ports), protocol (postcard wire types), application (config service, data projection), adapters (config-memory, tcp-server), bootstrap (composition root with fake data). Client: client-domain (layout engine, render tree, HAL ports), client-application (message handling, repaint commands), adapters (tcp-client, display-terminal), client-desktop (end-to-end working). ESP32: client-esp32 firmware with ILI9341 display over SPI, WiFi networking. Display test verified on hardware — landscape orientation, text rendering works. 60 workspace tests, all passing.
83 lines
2.4 KiB
Rust
83 lines
2.4 KiB
Rust
use std::collections::BTreeMap;
|
|
use domain::{
|
|
DisplayHint, KeyMapping, Value, WidgetConfig, WidgetId, WidgetState,
|
|
};
|
|
use application::DataProjection;
|
|
|
|
fn weather_widget() -> WidgetConfig {
|
|
WidgetConfig::new(
|
|
1,
|
|
"weather".into(),
|
|
DisplayHint::IconValue,
|
|
10,
|
|
vec![
|
|
KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into() },
|
|
KeyMapping { source_path: "$.icon".into(), target_key: "icon".into() },
|
|
],
|
|
)
|
|
}
|
|
|
|
fn weather_response(temp: f64) -> Value {
|
|
Value::Object(BTreeMap::from([
|
|
("temp".into(), Value::Number(temp)),
|
|
("icon".into(), Value::String("sunny".into())),
|
|
]))
|
|
}
|
|
|
|
#[test]
|
|
fn apply_poll_result_detects_new_widget_state() {
|
|
let mut projection = DataProjection::new();
|
|
let widgets = vec![weather_widget()];
|
|
|
|
let changed = projection.apply_poll_result(10, &weather_response(5.4), &widgets);
|
|
|
|
assert_eq!(changed.len(), 1);
|
|
assert_eq!(changed[0].0, 1);
|
|
assert_eq!(changed[0].1.data.get("temperature"), Some(&Value::Number(5.4)));
|
|
}
|
|
|
|
#[test]
|
|
fn apply_poll_result_returns_empty_when_nothing_changed() {
|
|
let mut projection = DataProjection::new();
|
|
let widgets = vec![weather_widget()];
|
|
|
|
projection.apply_poll_result(10, &weather_response(5.4), &widgets);
|
|
let changed = projection.apply_poll_result(10, &weather_response(5.4), &widgets);
|
|
|
|
assert!(changed.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn apply_poll_result_detects_changed_value() {
|
|
let mut projection = DataProjection::new();
|
|
let widgets = vec![weather_widget()];
|
|
|
|
projection.apply_poll_result(10, &weather_response(5.4), &widgets);
|
|
let changed = projection.apply_poll_result(10, &weather_response(6.1), &widgets);
|
|
|
|
assert_eq!(changed.len(), 1);
|
|
assert_eq!(changed[0].1.data.get("temperature"), Some(&Value::Number(6.1)));
|
|
}
|
|
|
|
#[test]
|
|
fn apply_poll_result_only_updates_widgets_bound_to_source() {
|
|
let mut projection = DataProjection::new();
|
|
let widgets = vec![
|
|
weather_widget(),
|
|
WidgetConfig::new(
|
|
2,
|
|
"portfolio".into(),
|
|
DisplayHint::KeyValue,
|
|
20,
|
|
vec![
|
|
KeyMapping { source_path: "$.value".into(), target_key: "amount".into() },
|
|
],
|
|
),
|
|
];
|
|
|
|
let changed = projection.apply_poll_result(10, &weather_response(5.4), &widgets);
|
|
|
|
assert_eq!(changed.len(), 1);
|
|
assert_eq!(changed[0].0, 1);
|
|
}
|