Files
Unlucky-Launcher/crates/app-iced/src/theme/tests.rs
T
AskaEth db541c820e feat(app-iced): Ink Cat / Silver Cat 换肤 —— 双色板默认 + 规格卡组件几何(M229)
产品出厂外观从 tone 派生的 `Scheme::baseline` 切到品牌色板:深色 = Ink(淡绿)、
浅色 = Silver(钢蓝),D38 取色链(背景图 / 壁纸)仍然优先,取色失败回落到色板。

主题三档接进设置页「通用 → 主题」:Ink / Silver / 跟随系统(落盘仍是
`appearance.theme_mode` 的 dark / light / auto,不新增配置键)。

组件几何按规格卡对齐(M228 的 `inkcat` 登记项逐条落地):
- 卡片圆角 12 → 16;实例元信息卡改用新的 ELEVATED CARD(无描边、SurfaceContainer
  底、内距 24、上标注 11/600)
- Snackbar 48/4 → 52/16(双行 72)
- Checkbox 20/rx2 → 18/rx4;Radio 20 → 18 Ø(内点仍 10)
- 按钮加 XS~XL 五档(产品仍只用 S=40),内距改走 ControlSize::pad_x
- 新增 FAB 四态(40/56/80/160×56)与图标按钮第四式 outlined
- 语言与主题两行 chips 统一成规格卡 chip(32 高 / rx8)

`theme.rs` 原本正好 400 行,先把单测纯搬移到 `theme/tests.rs` 再加色板字段;
`app/tests.rs`、`settings/general.rs` 同样按纯搬移拆分守 400 行硬线。

验证:cargo fmt --all --check、cargo check -p unlucky-app-iced --all-targets、
cargo test --workspace(除 clippy 前的一轮 flake,见报告)、cargo clippy --workspace
--all-targets -- -D warnings、全仓 400 行扫描。视觉矩阵 42 张在
DevDocs/artifacts/m229/(gitignored,不进提交)。
2026-11-11 01:49:13 +08:00

208 lines
7.5 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.
//! `theme` 的单元测试(M229 从 `theme.rs` 纯搬移,守 400 行硬线:加双色板字段前该文件正好 400 行)。
//!
//! 内容一行未改,只把 `mod tests { … }` 的包裹层换成独立文件。
use super::*;
const FAMILIES: Families = Families {
text: "Segoe UI Variable Text",
display: "Segoe UI Variable Display",
mono: "Cascadia Mono",
};
fn ui(flavor: SchemeFlavor) -> Ui {
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
}
#[test]
fn theme_name_tracks_the_flavor() {
assert_eq!(ui(SchemeFlavor::Light).name(), "ull-silver");
assert_eq!(ui(SchemeFlavor::Dark).name(), "ull-ink");
assert_ne!(
ui(SchemeFlavor::Light).theme().to_string(),
ui(SchemeFlavor::Dark).theme().to_string()
);
}
#[test]
fn canvas_card_overlay_follow_surface_containers() {
let ui = ui(SchemeFlavor::Light);
assert_eq!(ui.layer(MaterialLayer::Canvas), ui.color(Role::Surface));
assert_eq!(
ui.layer(MaterialLayer::Card),
ui.color(Role::SurfaceContainerLow)
);
assert_eq!(
ui.layer(MaterialLayer::Overlay),
ui.color(Role::SurfaceContainerHigh)
);
}
#[test]
fn radii_only_come_from_the_scale() {
let ui = ui(SchemeFlavor::Light);
// 卡片 Medium(12):边长够大时就是 12
assert_eq!(ui.radius_dp(ShapeScale::Medium).top_left, 12.0);
assert_eq!(ui.radius_dp(ShapeScale::ExtraSmall).top_left, 4.0);
// 药丸 Full:按边长一半折算
assert_eq!(ui.radius(ShapeScale::Full, 32.0).top_left, 16.0);
// 不许超过边长一半
assert_eq!(ui.radius(ShapeScale::ExtraLarge, 20.0).top_left, 10.0);
}
#[test]
fn outlines_are_one_pixel_and_use_outline_variant() {
let ui = ui(SchemeFlavor::Light);
let border = ui.outlined(ShapeScale::Medium);
assert_eq!(border.width, STROKE_PX);
assert_eq!(border.width, 1.0, "rubric G7:全站 1px");
assert_eq!(border.color, ui.color(Role::OutlineVariant));
assert_eq!(border.radius.top_left, 12.0);
}
#[test]
fn emphasized_weights_are_the_official_ones() {
let ui = ui(SchemeFlavor::Light);
// title-small 的强调档是 700(官方 Emphasized)
assert_eq!(
ui.emphasized_font(TypeRole::TitleSmall).weight,
iced::font::Weight::Bold
);
// baseline 正文是 400
assert_eq!(
ui.font(TypeRole::BodyMedium).weight,
iced::font::Weight::Normal
);
}
#[test]
fn body_text_keeps_wcag_contrast_in_both_flavors() {
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let scheme = Scheme::baseline(flavor);
assert!(
scheme.contrast(Role::OnSurface, Role::Surface) >= 4.5,
"{flavor:?} 的正文对比度不够"
);
assert!(
scheme.contrast(Role::OnSecondaryContainer, Role::SecondaryContainer) >= 4.5,
"{flavor:?} 的选中态药丸对比度不够"
);
}
}
#[test]
fn every_role_maps_to_an_iced_color() {
for role in Role::ALL {
let color = ui(SchemeFlavor::Dark).color(role);
assert!((0.0..=1.0).contains(&color.a), "{role:?} 的 alpha 越界");
}
}
#[test]
fn state_layers_are_opaque_so_cpu_and_gpu_match() {
let ui = ui(SchemeFlavor::Light);
let hovered = ui.state_over(StateLayer::Hover, Role::OnSurface, Role::Surface);
assert_eq!(hovered.a, 1.0, "状态层必须给出不透明结果");
assert_ne!(hovered, ui.color(Role::Surface));
}
/// D38:配色方案必须真的换色;显式传 baseline 时逐字段等于 baseline。
/// 抓不住:取色好不好看、设置页是否真切过去(后者由 app 侧日志举证)。
#[test]
fn a_resolved_scheme_replaces_the_baseline_colours() {
use unlucky_design::palette::{resolve_source, CardKey, ImageInput};
// 4×4 的纯红图:种子彩度远高于门槛,必然走到「背景图取色」这一级。
let pixels = [0xC0_u8, 0x20, 0x20, 0xFF].repeat(16);
let input = ImageInput::pixels(&pixels, 4, 4);
let (scheme, provenance) =
resolve_source(Some(input), None, Some(CardKey::Sky), SchemeFlavor::Light);
assert_eq!(
provenance.level,
unlucky_design::palette::Level::BackgroundImage,
"纯红图必须取色成功(否则本测试的输入不合格)"
);
let tinted = Ui::from_scheme(scheme, FAMILIES);
let board = ui(SchemeFlavor::Light);
assert_ne!(
tinted.color(Role::Primary),
board.color(Role::Primary),
"取色方案必须换掉 primary"
);
// 深浅仍跟着方案走(方案自带 flavor),不会把 Light 变成 Dark;色板名也仍说实话。
assert_eq!(tinted.flavor(), SchemeFlavor::Light);
assert_eq!(tinted.name(), board.name());
// 显式传 baseline 时逐块等于 baseline 口径(M229 之后产品默认不再是它,但这条通路必须诚实)。
let explicit = Ui::from_scheme(Scheme::baseline(SchemeFlavor::Light), FAMILIES);
let baseline = Ui::baseline(SchemeFlavor::Light, FAMILIES);
for role in Role::ALL {
assert_eq!(
explicit.color(role),
baseline.color(role),
"{role:?}:显式传 baseline 时颜色必须与 baseline 口径一致"
);
}
}
/// M229:`Ui::new` 走**品牌色板**(Ink 深 / Silver 浅),不再走 tone 派生的 `baseline`。
///
/// 抓得住:换肤没接上(色值仍是 baseline)、深浅映射反了(Light 拿到 Ink)。
/// 抓不住:色板好不好看、页面有没有绕过 `Ui::color` 自己写色值(后者由 `no_hardcoded_tokens` 扫)。
#[test]
fn every_ui_is_built_from_a_brand_board() {
for (flavor, board) in [
(SchemeFlavor::Light, BrandBoard::Silver),
(SchemeFlavor::Dark, BrandBoard::Ink),
] {
let evaluator = ui(flavor);
assert_eq!(evaluator.board(), board, "{flavor:?} 该用 {:?}", board);
assert_eq!(evaluator.name(), board_name(board));
for role in Role::ALL {
assert_eq!(
evaluator.color(role),
to_iced(board.get(role)),
"{flavor:?} 的 {role:?} 不是色板值"
);
}
// 与 tone 派生的 baseline 确实不同(否则「换肤」这件事没发生)。
let baseline = Ui::baseline(flavor, FAMILIES);
assert_ne!(
evaluator.color(Role::Primary),
baseline.color(Role::Primary),
"{flavor:?}:色板与 baseline 的 primary 撞了,换肤等于没接"
);
assert_eq!(evaluator.flavor(), flavor);
}
}
/// 显式选板时深浅跟着板走(`Ink = Dark` / `Silver = Light`),「跟随系统」不必另造一种板。
#[test]
fn picking_a_board_sets_the_flavor_from_the_board() {
let ink = Ui::with_board(BrandBoard::Ink, Platform::Windows, FAMILIES);
let silver = Ui::with_board(BrandBoard::Silver, Platform::Windows, FAMILIES);
assert_eq!(ink.flavor(), SchemeFlavor::Dark);
assert_eq!(silver.flavor(), SchemeFlavor::Light);
assert_ne!(ink.theme().to_string(), silver.theme().to_string());
}
/// 卡片方框走规格卡(`rx=16`),不是组件表的 `Medium(12)`。
#[test]
fn cards_take_the_spec_corner() {
let evaluator = ui(SchemeFlavor::Light);
assert_eq!(evaluator.card_shape(), ShapeScale::Large);
assert_eq!(evaluator.card_box().radius_px(), 16.0);
assert_eq!(
evaluator.outlined(evaluator.card_shape()).radius.top_left,
16.0
);
}
fn board_name(board: BrandBoard) -> &'static str {
match board {
BrandBoard::Ink => "ull-ink",
BrandBoard::Silver => "ull-silver",
}
}