Files
luma-home/src/weather-tests.rs
T
Kaloyan DanchevandClaude Opus 4.6 cbea5a322f
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s
Merge Luma launcher fork into monorepo
Move trampoline files (boot-hook, watcher, overlay) into trampoline/
subdirectory. Add upstream Luma v2.3.1 source at repo root with our
additions:

- Page::Ha (4th nav tab) with placeholder empty state
- HA tab config schema and default entity mapping (ha-home/)
- Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings
- All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-24 11:52:55 +03:00

237 lines
8.0 KiB
Rust

use super::*;
use serde_json::json;
fn place() -> Location {
Location {
name: "Vienna".into(),
region: "Vienna".into(),
country: "Austria".into(),
latitude: 48.2085,
longitude: 16.3721,
}
}
fn reading() -> Reading {
Reading {
temperature: 14.2,
code: 2,
is_day: true,
}
}
fn preferences() -> Preferences {
Preferences {
weather_location: Some(place()),
..Preferences::default()
}
}
fn cache(updated_at: i64) -> Cache {
Cache {
key: place().key(),
updated_at,
reading: reading(),
}
}
#[test]
fn search_results_are_validated_normalized_and_deduplicated() {
let result = parse_locations(&json!({"results":[
{"name":" Vienna\n", "admin1":"Vienna", "country":"Austria", "latitude":48.2085,"longitude":16.3721},
{"name":"Duplicate", "latitude":48.2085,"longitude":16.3721},
{"name":"Bad", "latitude":91,"longitude":16}, {"name":"", "latitude":0,"longitude":0}
]})).unwrap();
assert_eq!(result, vec![place()]);
assert_eq!(result[0].detail(), "Austria");
assert!(parse_locations(&json!({})).unwrap().is_empty());
assert!(parse_locations(&json!({"results":"wrong"})).is_err());
assert!(parse_locations(&json!({"error":true})).is_err());
}
#[test]
fn temperatures_convert_the_unrounded_reading_and_use_web_rounding() {
assert_eq!(format_temperature(14.2, Unit::Fahrenheit), "58°F");
assert_eq!(format_temperature(14.2, Unit::Celsius), "14°C");
assert_eq!(format_temperature(-0.5, Unit::Celsius), "0°C");
assert_eq!(format_temperature(-5.5, Unit::Celsius), "-5°C");
}
#[test]
fn api_readings_and_conditions_cover_invalid_data_and_night() {
let good = json!({"current":{"temperature_2m":14.2,"weather_code":2.0,"is_day":1.0}});
assert_eq!(parse_reading(&good).unwrap(), reading());
for value in [
json!({}),
json!({"current":{"temperature_2m":90,"weather_code":2,"is_day":1}}),
json!({"current":{"temperature_2m":14,"weather_code":2.5,"is_day":1}}),
json!({"current":{"temperature_2m":14,"weather_code":2,"is_day":true}}),
] {
assert!(parse_reading(&value).is_err());
}
for (code, day, icon) in [
(0, false, "moon"),
(0, true, "sun"),
(45, true, "fog"),
(56, true, "drizzle"),
(81, true, "rain"),
(85, true, "snow"),
(99, true, "storm"),
] {
assert_eq!(
condition(Reading {
code,
is_day: day,
..reading()
})
.icon,
icon
);
}
}
#[test]
fn cache_reuse_and_units_do_not_trigger_another_request() {
let mut weather = Weather {
cache: Some(cache(1000)),
..Weather::default()
};
let mut preferences = preferences();
assert!(weather.sync(&preferences, true, 1100).0.is_none());
preferences.temperature_unit = Unit::Fahrenheit;
assert!(weather.sync(&preferences, true, 1200).0.is_none());
assert_eq!(weather.reading(&place()).unwrap().temperature, 14.2);
assert!(matches!(
weather.sync(&preferences, true, 1900).0,
Some(Command::Forecast { .. })
));
}
#[test]
fn no_requests_without_a_location_enabled_widget_and_visible_home() {
let mut weather = Weather::default();
let mut preferences = preferences();
preferences.show_weather = false;
assert!(weather.sync(&preferences, true, 1000).0.is_none());
assert!(preferences.weather_location.is_some());
preferences.show_weather = true;
assert!(weather.sync(&preferences, false, 1000).0.is_none());
preferences.weather_location = None;
assert!(weather.sync(&preferences, true, 1000).0.is_none());
}
#[test]
fn weather_failure_keeps_valid_cache_labels_it_saved_and_retries_later() {
let mut weather = Weather {
cache: Some(cache(1000)),
..Weather::default()
};
let preferences = preferences();
let Some(Command::Forecast { id, location }) = weather.sync(&preferences, true, 2000).0 else {
panic!("Expected stale-cache refresh");
};
weather.accept_forecast(id, &location.key(), Err("offline".into()), 2000);
assert!(weather.failed);
assert!(weather.reading(&place()).is_some());
assert!(weather.sync(&preferences, true, 2299).0.is_none());
assert!(weather.sync(&preferences, true, 2300).0.is_some());
assert!(cache(1000).age(999).is_none());
assert!(cache(1000).age(1001 + MAX_CACHE_AGE).is_none());
weather.sync(&preferences, true, 1001 + MAX_CACHE_AGE);
assert!(weather.cache.is_none());
}
#[test]
fn removed_disabled_or_changed_locations_reject_in_flight_results() {
let mut weather = Weather::default();
let mut preferences = preferences();
let Some(Command::Forecast { id, location }) = weather.sync(&preferences, true, 1000).0 else {
panic!();
};
weather.remove_location();
preferences.weather_location = None;
assert_eq!(
weather.accept_forecast(id, &location.key(), Ok(reading()), 1001),
(false, None)
);
assert!(weather.cache.is_none());
preferences.weather_location = Some(place());
let Some(Command::Forecast { id, location }) = weather.sync(&preferences, true, 1002).0 else {
panic!();
};
preferences.show_weather = false;
weather.sync(&preferences, true, 1003);
assert_eq!(
weather.accept_forecast(id, &location.key(), Ok(reading()), 1004),
(false, None)
);
preferences.show_weather = true;
let Some(Command::Forecast {
id: old,
location: old_location,
}) = weather.sync(&preferences, true, 1005).0
else {
panic!();
};
preferences.weather_location.as_mut().unwrap().latitude += 1.;
let Some(Command::Forecast {
id: new,
location: new_location,
}) = weather.sync(&preferences, true, 1006).0
else {
panic!();
};
assert_eq!(
weather.accept_forecast(old, &old_location.key(), Ok(reading()), 1007),
(false, None)
);
assert!(
weather
.accept_forecast(new, &new_location.key(), Ok(reading()), 1007)
.0
);
assert_eq!(weather.cache.as_ref().unwrap().key, new_location.key());
}
#[test]
fn returning_home_retries_stale_weather_without_waiting_for_the_old_retry_timer() {
let mut weather = Weather {
cache: Some(cache(1000)),
..Weather::default()
};
let preferences = preferences();
let Some(Command::Forecast { id, location }) = weather.sync(&preferences, true, 2000).0 else {
panic!();
};
weather.accept_forecast(id, &location.key(), Err("offline".into()), 2000);
assert!(weather.sync(&preferences, false, 2010).0.is_none());
assert!(weather.sync(&preferences, true, 2020).0.is_some());
}
#[test]
fn editing_query_invalidates_old_results_and_short_queries_do_not_search() {
let mut weather = Weather::default();
weather.edit("V".into());
assert!(weather.search(Language::En).is_none());
assert!(weather.message.is_some());
weather.edit("Vienna".into());
let Some(Command::Search { id, .. }) = weather.search(Language::En) else {
panic!();
};
assert!(weather.search(Language::En).is_none());
weather.edit("Paris".into());
assert!(!weather.accept_search(id, Ok(vec![place()])));
assert!(weather.results.is_empty());
}
#[test]
fn malformed_weather_preferences_do_not_lose_existing_favorites() {
let preferences: Preferences = serde_json::from_value(json!({
"favorites":["netflix"], "weather_location":{"name":"Invalid","latitude":"oops","longitude":0},
"temperature_unit":"kelvin", "show_weather":"off"
})).unwrap();
assert_eq!(preferences.favorites, Some(vec!["netflix".into()]));
assert!(preferences.weather_location.is_none());
assert_eq!(preferences.temperature_unit, Unit::Celsius);
assert!(preferences.show_weather);
let old: Preferences = serde_json::from_value(json!({"background":"forest"})).unwrap();
assert!(old.show_weather);
assert!(old.weather_location.is_none());
}