✨ (pagetop): Mejora la elección de zona horaria
- Nuevas opciones `app.timezone_regions`, `app.timezone_order` y `app.timezone_per_user` para limitar y ordenar las regiones ofrecidas y decidir si cada usuario puede tener su propia zona horaria. - Añade `TzRegion` y `TimezoneOrder`; `Timezone::supported_by_region()` devuelve ahora `TzRegion` y excluye los alias de la base IANA que repiten otra zona (tzdata 2025b). - Añade `Timezone::is_supported_in()` para validar con otras regiones. - `SelectTimezone` admite `with_regions()`, `with_order()` y `with_utc_offset()`, muestra nombres legibles y conserva la zona actual aunque ya no se ofrezca. - `app.timezone` se resuelve sin distinguir mayúsculas. - pagetop-user no ofrece idioma ni zona horaria si no se aplican y conserva lo guardado; acepta la zona actual aunque ya no se ofrezca.
This commit is contained in:
parent
f819181872
commit
346714ab29
19 changed files with 1257 additions and 131 deletions
|
|
@ -61,3 +61,175 @@ async fn required_field_asks_to_choose_only_when_nothing_valid_is_selected() {
|
|||
let html = chosen.render(&mut Context::default()).await.into_string();
|
||||
assert!(!html.contains(r#"value="""#));
|
||||
}
|
||||
|
||||
// Zones without daylight saving time keep the expected labels stable all year round.
|
||||
#[pagetop::test]
|
||||
async fn utc_offset_is_shown_only_when_enabled() {
|
||||
let mut plain = form::SelectTimezone::new();
|
||||
let html = plain.render(&mut Context::default()).await.into_string();
|
||||
assert!(html.contains(r#"<option value="Asia/Kolkata">Asia/Kolkata</option>"#));
|
||||
assert!(!html.contains("(UTC"));
|
||||
|
||||
let mut with_offset = form::SelectTimezone::new().with_utc_offset(true);
|
||||
let html = with_offset
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
assert!(html.contains(r#"<option value="Asia/Kolkata">Asia/Kolkata (UTC+05:30)</option>"#));
|
||||
assert!(html.contains(r#"<option value="America/Bogota">America/Bogota (UTC-05:00)</option>"#));
|
||||
assert!(html.contains(r#"<option value="Etc/UTC">Etc/UTC (UTC+00:00)</option>"#));
|
||||
}
|
||||
|
||||
// Zones of a region are sorted by name, or by offset and then by name when offsets are shown:
|
||||
// Kathmandu (+05:45) comes after Kolkata (+05:30), and Seoul precedes Tokyo (both +09:00).
|
||||
#[pagetop::test]
|
||||
async fn zones_are_sorted_by_offset_and_then_by_name_when_offsets_are_shown() {
|
||||
let positions = |html: &str| -> Vec<usize> {
|
||||
[
|
||||
"Asia/Dubai",
|
||||
"Asia/Kathmandu",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Seoul",
|
||||
"Asia/Tokyo",
|
||||
]
|
||||
.iter()
|
||||
.map(|zone| html.find(&format!(r#"value="{zone}""#)).unwrap())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut plain = form::SelectTimezone::new();
|
||||
let html = plain.render(&mut Context::default()).await.into_string();
|
||||
let [dubai, kathmandu, kolkata, seoul, tokyo] = positions(&html)[..] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(dubai < kathmandu && kathmandu < kolkata && kolkata < seoul && seoul < tokyo);
|
||||
|
||||
let mut with_offset = form::SelectTimezone::new().with_utc_offset(true);
|
||||
let html = with_offset
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
let [dubai, kathmandu, kolkata, seoul, tokyo] = positions(&html)[..] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(dubai < kolkata && kolkata < kathmandu && kathmandu < seoul && seoul < tokyo);
|
||||
}
|
||||
|
||||
fn group_labels(html: &str) -> Vec<&str> {
|
||||
html.split(r#"<optgroup label=""#)
|
||||
.skip(1)
|
||||
.map(|group| group.split('"').next().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn defaults_come_from_the_global_settings() {
|
||||
let field = form::SelectTimezone::new();
|
||||
assert_eq!(*field.order(), global::SETTINGS.app.timezone_order);
|
||||
let regions: Vec<TzRegion> = global::SETTINGS
|
||||
.app
|
||||
.timezone_regions
|
||||
.split(',')
|
||||
.filter_map(TzRegion::from_name)
|
||||
.collect();
|
||||
assert_eq!(field.regions(), ®ions);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn with_regions_limits_the_offered_regions_and_keeps_etc() {
|
||||
let mut field = form::SelectTimezone::new().with_regions([TzRegion::Europe]);
|
||||
let html = field.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert_eq!(group_labels(&html), ["Europe", "Other"]);
|
||||
assert!(html.contains(r#"value="Europe/Madrid""#));
|
||||
assert!(!html.contains(r#"value="America/Bogota""#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn with_order_listed_follows_the_given_regions() {
|
||||
let mut field = form::SelectTimezone::new()
|
||||
.with_order(global::TimezoneOrder::Listed)
|
||||
.with_regions([TzRegion::Pacific, TzRegion::Europe, TzRegion::Asia]);
|
||||
let html = field.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert_eq!(group_labels(&html), ["Pacific", "Europe", "Asia", "Other"]);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn with_order_alphabetical_sorts_by_translated_name() {
|
||||
let mut field = form::SelectTimezone::new()
|
||||
.with_order(global::TimezoneOrder::Alphabetical)
|
||||
.with_regions([
|
||||
TzRegion::Pacific,
|
||||
TzRegion::Arctic,
|
||||
TzRegion::Indian,
|
||||
TzRegion::Europe,
|
||||
TzRegion::Asia,
|
||||
]);
|
||||
let mut cx = Context::default().with_langid(&Locale::resolve("es-ES"));
|
||||
let html = field.render(&mut cx).await.into_string();
|
||||
|
||||
assert_eq!(
|
||||
group_labels(&html),
|
||||
["Ártico", "Asia", "Europa", "Índico", "Pacífico", "Otras"]
|
||||
);
|
||||
}
|
||||
|
||||
// A valid zone that is no longer offered stays selectable, so saving the form keeps it.
|
||||
#[pagetop::test]
|
||||
async fn a_valid_but_unoffered_selected_zone_is_kept_in_its_own_group() {
|
||||
let mut field = form::SelectTimezone::new()
|
||||
.with_regions([TzRegion::Europe])
|
||||
.with_selected("America/Bogota");
|
||||
let html = field.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert_eq!(
|
||||
group_labels(&html),
|
||||
["Current time zone", "Europe", "Other"]
|
||||
);
|
||||
assert!(html.contains(r#"<option value="America/Bogota" selected>America/Bogota</option>"#));
|
||||
assert!(!html.contains(r#"<option value="" selected>"#));
|
||||
|
||||
let mut legacy = form::SelectTimezone::new().with_selected("US/Eastern");
|
||||
let html = legacy.render(&mut Context::default()).await.into_string();
|
||||
assert_eq!(group_labels(&html).first(), Some(&"Current time zone"));
|
||||
assert!(html.contains(r#"<option value="US/Eastern" selected>US/Eastern</option>"#));
|
||||
|
||||
let mut required = form::SelectTimezone::new()
|
||||
.with_required(true)
|
||||
.with_regions([TzRegion::Europe])
|
||||
.with_selected("America/Bogota");
|
||||
let html = required.render(&mut Context::default()).await.into_string();
|
||||
assert!(!html.contains(r#"value="""#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn an_invalid_selected_zone_is_not_offered() {
|
||||
let mut field = form::SelectTimezone::new().with_selected("Mars/Olympus");
|
||||
let html = field.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(!html.contains("Current time zone"));
|
||||
assert!(!html.contains("Mars/Olympus"));
|
||||
assert!(html.contains(r#"<option value="" selected>"#));
|
||||
}
|
||||
|
||||
// Labels replace underscores with spaces; values keep the IANA name.
|
||||
#[pagetop::test]
|
||||
async fn labels_are_readable_while_values_keep_the_iana_name() {
|
||||
let mut field = form::SelectTimezone::new();
|
||||
let html = field.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"<option value="America/New_York">America/New York</option>"#));
|
||||
assert!(html.contains(
|
||||
r#"<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos Aires</option>"#
|
||||
));
|
||||
assert!(html.contains(r#"<option value="Etc/UTC">Etc/UTC</option>"#));
|
||||
|
||||
let mut current = form::SelectTimezone::new()
|
||||
.with_regions([TzRegion::Europe])
|
||||
.with_selected("America/New_York");
|
||||
let html = current.render(&mut Context::default()).await.into_string();
|
||||
assert!(
|
||||
html.contains(r#"<option value="America/New_York" selected>America/New York</option>"#)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue