Files
Unlucky-Launcher/crates/core/tests/setting/appearance_keys.rs
T
AskaEth 8a8e5ca0c0
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Unlucky Launcher v0.2.0
首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
2026-11-05 14:22:11 +08:00

205 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 「外观(高级)」键簇(`setting::appearance` + `keys::launcher_schema` 的 appearance.* 段)。
//!
//! 守三条设计约束(09-ui-layout §111):
//! 1. 一切高级特性**默认关闭**(出厂 Metro 纯色)——默认值即出厂态;
//! 2. 全部走 D6 schema 且 **AI 可写**(白名单内,可被 agent 调节);
//! 3. 值域有硬边界(滑条夹取),坏值/手改配置不崩(回落默认)。
use serde_json::json;
use unlucky_core::setting::appearance::{
hex_to_rgb, normalize_hex, relative_luminance, Appearance, BgFill, DEFAULT_ACCENT,
PANEL_ALPHA_MIN,
};
use unlucky_core::setting::document::SettingDocument;
use unlucky_core::setting::keys::{launcher_schema, launcher_version};
use unlucky_core::setting::spec::ValueKind;
fn doc(raw: serde_json::Value) -> SettingDocument {
SettingDocument::load(&launcher_schema(), &raw).0
}
#[test]
fn factory_defaults_are_all_advanced_features_off() {
let appearance = Appearance::from_store(&unlucky_core::setting::store::SettingStore::load(
std::env::temp_dir().join(format!("ull-appearance-none-{}.json", std::process::id())),
launcher_schema(),
));
assert_eq!(appearance.theme_mode, "auto", "D17:出厂跟随系统");
assert!(!appearance.bg_image_enabled, "背景图默认关");
assert!(!appearance.bg_image_active());
assert_eq!(appearance.bg_fill, BgFill::Cover);
assert_eq!(appearance.panel_alpha, 1.0, "出厂完全不透明(Metro 纯色)");
assert!(!appearance.glass, "玻璃拟态默认关");
assert_eq!(appearance.accent_color, "", "空串 = 未自定义");
assert_eq!(appearance.accent_or_default(), DEFAULT_ACCENT);
}
#[test]
fn reading_a_configured_document_reflects_each_key() {
let document = doc(json!({
"appearance": {
"theme_mode": "dark",
"bg_image_enabled": true,
"bg_image": "D:/wall/paper.png",
"bg_fill": "tile",
"bg_dim": 0.6,
"panel_alpha": 0.8,
"glass": true,
"accent_color": "#ff5722",
}
}));
let appearance = Appearance::read(&document);
assert_eq!(appearance.theme_mode, "dark");
assert!(appearance.bg_image_active());
assert_eq!(appearance.bg_image, "D:/wall/paper.png");
assert_eq!(appearance.bg_fill, BgFill::Tile);
assert!((appearance.bg_dim - 0.6).abs() < 1e-9);
assert!((appearance.panel_alpha - 0.8).abs() < 1e-9);
assert!(appearance.glass);
assert_eq!(appearance.accent_color, "#FF5722", "读入即规范化大写");
assert_eq!(appearance.accent_or_default(), "#FF5722");
}
#[test]
fn bad_values_fall_back_instead_of_panicking() {
// 手改坏的配置:枚举拼错 / 开关类型错 / accent 不是 #RRGGBB。
let document = doc(json!({
"appearance": {
"bg_fill": "stretch",
"bg_image_enabled": "yes",
"accent_color": "rebeccapurple",
}
}));
let appearance = Appearance::read(&document);
assert_eq!(appearance.bg_fill, BgFill::Cover);
assert!(!appearance.bg_image_enabled);
assert_eq!(appearance.accent_color, "");
assert_eq!(appearance.accent_or_default(), DEFAULT_ACCENT);
}
#[test]
fn panel_alpha_is_clamped_to_the_contrast_floor() {
// schema 下限 0.4;读侧再夹一次(挡手改文件绕过 schema 的情况)。
let document = doc(json!({ "appearance": { "panel_alpha": 0.05 } }));
assert!((Appearance::read(&document).panel_alpha - PANEL_ALPHA_MIN).abs() < 1e-9);
}
#[test]
fn appearance_keys_are_all_ai_writable_and_registered() {
let schema = launcher_schema();
// 主题三态是 D17 已定案的最轻量键(silent/Low),其余高级键留审计痕迹(audited/Medium)。
let audited_keys = [
"appearance.bg_image_enabled",
"appearance.bg_image",
"appearance.bg_fill",
"appearance.bg_dim",
"appearance.panel_alpha",
"appearance.glass",
"appearance.accent_color",
// M91(D36/D37/D38)的外观下层三键,同样 audited。
"appearance.window_opacity",
"appearance.titlebar_mode",
"appearance.palette_source",
];
for key in audited_keys {
let spec = schema.spec(key).unwrap_or_else(|| panic!("{key} 必须登记"));
assert!(spec.ai.writable, "{key} 必须在 AI 可写白名单内");
// 纯视觉、可一键复原 → 不要求弹窗确认(High 级留给凭据/数据根/代理)。
assert!(
!spec.ai.sensitivity.requires_confirmation(),
"{key} 不该要确认"
);
assert_eq!(
spec.ai.sensitivity.as_str(),
"medium",
"{key} 要留审计痕迹(AI 改过什么可回看)"
);
}
// 主题三态仍是 silent(D17 已定案的最轻量键)。
assert_eq!(
schema
.spec("appearance.theme_mode")
.unwrap()
.ai
.sensitivity
.as_str(),
"low"
);
}
#[test]
fn schema_registration_shape_is_right() {
assert_eq!(
launcher_version().minor,
5,
"1.5.0 登记 M116 的窗口圆角策略键"
);
let schema = launcher_schema();
assert_eq!(
schema.spec("appearance.bg_fill").unwrap().kind,
ValueKind::Enum(&["cover", "contain", "tile"])
);
assert!(matches!(
schema.spec("appearance.panel_alpha").unwrap().kind,
ValueKind::Float { min, max } if (min - PANEL_ALPHA_MIN).abs() < 1e-9 && (max - 1.0).abs() < 1e-9
));
assert_eq!(
schema.spec("appearance.accent_color").unwrap().kind,
ValueKind::Text { max_len: 9 },
"#RRGGBB 最多 7 字符,留 9 容错带井号与空白"
);
// 默认值 = 出厂态
assert_eq!(
schema.spec("appearance.panel_alpha").unwrap().default,
serde_json::Value::from(1.0)
);
assert_eq!(
schema.spec("appearance.glass").unwrap().default,
serde_json::Value::from(false)
);
}
#[test]
fn write_round_trips_through_the_store() {
let temp =
std::env::temp_dir().join(format!("ull-appearance-{}-{}", std::process::id(), line!()));
std::fs::create_dir_all(temp.join("config")).expect("临时目录");
let path = temp.join("config/launcher.json");
let _ = std::fs::remove_file(&path);
let mut store = unlucky_core::setting::store::SettingStore::load(&path, launcher_schema());
let mut appearance = Appearance::from_store(&store);
appearance.theme_mode = "dark".to_owned();
appearance.panel_alpha = 0.8;
appearance.bg_image_enabled = true;
appearance.bg_image = "D:/wall/p.png".to_owned();
appearance.accent_color = "#0f6cbd".to_owned();
appearance.write(&mut store).expect("写外观键");
store.save().expect("落盘");
let reloaded = Appearance::from_store(&unlucky_core::setting::store::SettingStore::load(
&path,
launcher_schema(),
));
assert_eq!(reloaded.theme_mode, "dark");
assert!((reloaded.panel_alpha - 0.8).abs() < 1e-9);
assert!(reloaded.bg_image_active());
assert_eq!(reloaded.accent_color, "#0F6CBD");
std::fs::remove_dir_all(&temp).ok();
}
#[test]
fn hex_helpers_validate_and_convert() {
assert_eq!(normalize_hex(" 0f6cbd "), Some("#0F6CBD".to_owned()));
assert_eq!(normalize_hex("#0F6CBD"), Some("#0F6CBD".to_owned()));
assert_eq!(normalize_hex("#0F6CB"), None);
assert_eq!(normalize_hex("#GGGGGG"), None);
assert_eq!(hex_to_rgb("#0078D4"), Some((0x00, 0x78, 0xD4)));
// WCAG 相对亮度:纯黑 0、纯白 1、Metro 蓝居中偏暗。
assert!(relative_luminance("#000000").unwrap().abs() < 1e-9);
assert!((relative_luminance("#FFFFFF").unwrap() - 1.0).abs() < 1e-9);
let metro = relative_luminance(DEFAULT_ACCENT).unwrap();
assert!(metro > 0.05 && metro < 0.3, "Metro 蓝亮度 {metro}");
}