Files
Unlucky-Launcher/crates/app-iced/src/components/buttons/tests/roles.rs
T
AskaEth a5d1dec495
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
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
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
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
2026-11-11 21:41:38 +08:00

191 lines
8.3 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.
//! 角色语义 + 「可主题化」断言(M248:Elevated 底 / Outlined 描边)。
//!
//! **判据不是「等于某个 hex」**:品牌色板本来就会被换(D38 的取色方案会整表接管 49 个角色),
//! 钉死色值等于把「配色可自定义」这条路焊上。所以这里钉两件互相补位的事:
//!
//! 1. [`role_table_is_named_roles_not_colours`]:`Variant::roles()` 吐的是**角色名**;Elevated 的
//! 容器还必须由**高程令牌**推导(`Elevation::Level1.surface_role()`),不许在组件里手抄一遍;
//! 2. [`styles_follow_the_scheme_across_boards_and_custom_accents`]:同一变体在 Ink / Silver /
//! 自定义种子方案(`Scheme::from_seed`)三套方案下,`button::Style` 里解出的颜色**分别等于
//! 各自方案的该角色色值**,且三者互不相同。把「取角色」改成「取死值(`Role::Surface`)」
//! 或「写死某套方案」都会被这条打红。
//!
//! 抓不住什么:
//! - **令牌表里两个角色同值**时,换角色的变异只有第 1 条会红;
//! - **像素**:角色写对了、图上却是别的颜色(样式闭包被别处覆盖)——那是 `gallery/probe` 与出图的事。
use iced::widget::button;
use iced::Background;
use unlucky_design::color::{BrandBoard, Role, Scheme, SchemeFlavor};
use unlucky_design::elevation::Elevation;
use unlucky_design::typography::Platform;
use super::super::{style_closure, IconVariant, Variant};
use super::spec_of;
use crate::theme::{Ui, STROKE_PX};
/// 三套配色来源:两块出厂品牌板 + 一套 `Scheme::from_seed` 自定义种子方案。
///
/// 第三套是关键:品牌板是固化数据,**换不了**;只有「同一份组件代码吃进另一套方案就换色」
/// 才叫可自定义(D38 的取色链就是这么接管 49 个角色的)。种子用 design 里的
/// [`DEFAULT_SEED`](取色链默认的天蓝)——它是令牌,不是本文件写下的色值字面量
/// (`tests/no_hardcoded_tokens.rs` 拦 `#RRGGBB`,这条门禁是对的)。
fn themes() -> Vec<(&'static str, Ui)> {
vec![
(
"ink",
Ui::with_board(BrandBoard::Ink, Platform::Windows, super::FAMILIES),
),
(
"silver",
Ui::with_board(BrandBoard::Silver, Platform::Windows, super::FAMILIES),
),
(
"custom",
Ui::from_scheme(
Scheme::from_seed(SchemeFlavor::Dark, unlucky_design::color::DEFAULT_SEED),
super::FAMILIES,
),
),
]
}
#[test]
fn role_table_is_named_roles_not_colours() {
// ---- P2:Elevated 的容器是**高程令牌的推论**,不是一个手抄的角色名 ----------
assert_eq!(Variant::Elevated.elevation(), Elevation::Level1, "L1");
assert_eq!(
Variant::Elevated.roles().0,
Some(Elevation::Level1.surface_role()),
"Elevated 的底必须由高程令牌推导(L1 → surface_role = SurfaceContainerLow),\
组件里不许再写一遍这个角色名"
);
assert_ne!(
Elevation::Level1.surface_role(),
Role::Surface,
"L1 的容器面必须比画布高一档(否则 Elevated 与画布同色)"
);
// ---- P3:1dp 边界取 `Outline`;`OutlineVariant` 是装饰分隔线的角色 ----------
assert_eq!(
Variant::Outlined.roles(),
(None, Role::OnSurfaceVariant, Some(Role::Outline)),
"Outlined 的 1dp 描边取 Outline(`design/color/mod.rs` 的对比度纪律:\
Outline 是「非文字元素」角色、目标 3:1;OutlineVariant 是纯装饰分隔线)"
);
// 同族一致性:图标按钮那一式一直是 Outline,此前文字按钮是 OutlineVariant ⇒ 全站两套描边
assert_eq!(
IconVariant::Outlined.roles().2,
Some(Role::Outline),
"图标按钮 Outlined 与文字按钮 Outlined 必须是同一个描边角色"
);
}
/// 非文字元素的对比度下限 3:1(WCAG 1.4.11;`design/color/mod.rs` 的对比度纪律同一口径)。
///
/// 这条是 P3 的**理由**而不只是判据:`OutlineVariant` 在深/浅两块板上都到不了 3:1,
/// 拿它画按钮边界等于「看得见形状、量不出边界」。别把下限写死成某个角色的实测值——
/// 换品牌板或换取色方案时,比例是**算出来的**,不是抄出来的。
#[test]
fn outlined_boundary_meets_the_non_text_contrast_bar() {
for (name, ui) in themes() {
let scheme = ui.scheme();
let boundary = scheme.contrast(Role::Outline, Role::Surface);
assert!(
boundary >= 3.0,
"{name}: Outline/Surface = {boundary:.2},非文字元素下限是 3:1"
);
// 报告用:装饰角色差多少(`--nocapture` 可见),不改判据
eprintln!(
"[{name}] Outline/Surface={boundary:.2} · OutlineVariant/Surface={:.2}",
scheme.contrast(Role::OutlineVariant, Role::Surface)
);
}
}
#[test]
fn styles_follow_the_scheme_across_boards_and_custom_accents() {
let elevated = spec_of(Variant::Elevated, Some(super::Interaction::Enabled));
let outlined = spec_of(Variant::Outlined, Some(super::Interaction::Enabled));
let mut elevated_fills = Vec::new();
let mut outlined_strokes = Vec::new();
for (name, ui) in themes() {
let style = style_closure(&ui, elevated);
let idle = style(&ui.theme(), button::Status::Active);
let expected = ui.color(Elevation::Level1.surface_role());
assert_eq!(
idle.background,
Some(Background::Color(expected)),
"{name}: Elevated 的底 = 本方案的 L1 容器面"
);
assert_ne!(
idle.background,
Some(Background::Color(ui.color(Role::Surface))),
"{name}: Elevated 退化成画布色了(取死值 Surface 的变异会打红这条)"
);
assert_eq!(
idle.text_color,
ui.color(Role::Primary),
"{name}: Elevated 标签 = Primary"
);
let style = style_closure(&ui, outlined);
let idle = style(&ui.theme(), button::Status::Active);
assert_eq!(
idle.border.color,
ui.color(Role::Outline),
"{name}: Outlined 描边 = Outline"
);
assert_eq!(idle.border.width, STROKE_PX, "{name}: 描边 1dp");
assert_eq!(
idle.text_color,
ui.color(Role::OnSurfaceVariant),
"{name}: Outlined 标签 = OnSurfaceVariant"
);
elevated_fills.push(expected);
outlined_strokes.push(idle.border.color);
}
// 三套方案两两不同 ⇒「跟着配色来源走」这件事有实据(不是三套恰好同色)
for (index, (name, _)) in themes().iter().enumerate() {
for other in index + 1..elevated_fills.len() {
assert_ne!(
elevated_fills[index], elevated_fills[other],
"{name} 与第 {other} 套方案的 Elevated 底同色,可主题化没有实据"
);
assert_ne!(
outlined_strokes[index], outlined_strokes[other],
"{name} 与第 {other} 套方案的 Outlined 描边同色"
);
}
}
}
/// 基线条:Ink(深)与 Silver(浅)两块**出厂**板必须给出不同的 Elevated 底与 Outlined 描边。
///
/// 与上一条分开写:上一条的三套方案里有一套是自定义的,这条只钉出厂外观——
/// 哪怕有人把 `themes()` 改成只跑自定义方案,出厂这块的回归也还在。
#[test]
fn ink_and_silver_resolve_the_same_roles_to_different_colours() {
let ink = Ui::with_board(BrandBoard::Ink, Platform::Windows, super::FAMILIES);
let silver = Ui::with_board(BrandBoard::Silver, Platform::Windows, super::FAMILIES);
for role in [
Elevation::Level1.surface_role(),
Role::Outline,
Role::OnSurfaceVariant,
] {
assert_ne!(
ink.color(role),
silver.color(role),
"{role:?} 在 Ink 与 Silver 上同色,说明它没跟着色板走"
);
}
// 浅色板的 L1 容器必须比画布暗、深色板必须比画布亮(层级方向不能反)
assert!(silver.color(Role::Surface).r > silver.color(Role::SurfaceContainerLow).r);
assert!(ink.color(Role::Surface).r < ink.color(Role::SurfaceContainerLow).r);
}