Files
Unlucky-Launcher/crates/app-iced/src/components/interaction/disabled/tests.rs
T
AskaEth 9bb3c33b8c fix(app-iced): 复审 r1 四条 P2 收口(M234)
- P2-1 判据恒绿:`gallery/probe/bases.rs` 的「反向检查」把字段档计数(`:172` 遮蔽后的
  `hits`)拿去比按钮档 stray ⇒ 恒绿。改成 `button_hits` / `field_hits` 两个计数,各比各的
  stray,失败信息的数值与单位对齐量测对象。变异自证:卡列里留一颗忘了 `.base(..)` 的按钮
  ⇒ 该断言单独红(2137 px vs 3178 px,退出码 101)。
- P2-2 FAB 的底错了:禁用态背景仍是满档 `PrimaryContainer`(`Disabled.layer() == None`),
  38% 内容却合在画布上。改成 `Base::of(PrimaryContainer)` + `on_container = false`
  (那一层 10% 在 FAB 身上不存在),两处调用收进一个 `content_color()`;新增值判据
  `disabled_fab_ink_composites_over_its_own_container`(修前 #8CA0AA / 修后 #548FBC 量级)。
- P2-3 漏改:`selection::paint::state_layer()`(hover/focus/pressed)与 `list_item` 的
  状态层叠底仍写死 `Role::Surface`,改成跟 `base`;同类两处(`assemble::transparent_state_color`
  的 Outlined/Text hover、drawer 项药丸)一并跟上。
- P2-4 文档失真:`feedback/{action,mod}.rs` 删掉「本地 α 合成 / 规则只认两档」的旧描述
  与失效链接;`list_item` 的旧签名;三处 `probe/base.rs` → `probe/bases.rs`;
  新增 `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints`
  真断言(原来只在文档里声称那条测试守 `BASE`),并把这句描述改准。
- nit:`disabledbase.rs` 字符串里塌成一行的续行空格、probe 汇总行把列左缘横坐标说成
  「像素计数」的措辞。

门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-234):fmt / check --all-targets /
test -p unlucky-app-iced(896 + 6 通过,0 失败)/ clippy --workspace --all-targets -D warnings /
文件 ≤400 全绿;probe 浅色与深色都过。
2026-11-11 06:34:24 +08:00

382 lines
17 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.
//! 禁用态两族 α 与合成色的单测(M229 复审 F1 后从 `disabled.rs` 纯搬移,守 400 行硬线)。
//!
//! 搬移之后**内容有改动**(不是一行未改):既有两条「α 族」测试的期望值从 `Scheme::baseline`
//! 改成**品牌板**(F1 的修法就是换色源),并新增 `disabled_colors_follow_the_brand_board_not_the_tone_baseline`
//! 作为 F1 的回归门——它与两条族测试放在一起,读的人一眼能看到「族」与「色源」两件事。
use super::*;
use unlucky_design::color::SchemeFlavor;
use unlucky_design::typography::Platform;
use crate::theme::Families;
const FAMILIES: Families = Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
};
fn ui(flavor: SchemeFlavor) -> Ui {
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
}
/// 两族的 α 必须都钉住(只钉一条,另一族被合并时会静默错 1/255)。
#[test]
fn both_disabled_alpha_classes_are_pinned_to_the_tokens() {
// 绑定成局部变量再过断言:直接比两个常量会被 clippy 判成「常量断言」。
let button_alpha = DISABLED_CONTAINER_ALPHA;
let surface_alpha = DISABLED_SURFACE_ALPHA;
let content_alpha = DISABLED_CONTENT_ALPHA;
// 按钮容器:官方 *ButtonTokens.DisabledContainerOpacity = 0.1f
assert!(
(button_alpha - 0.10).abs() < f32::EPSILON,
"按钮禁用容器 α 必须是 0.10(官方 DisabledContainerOpacity);\
0.12 是面状控件那一类,别混用"
);
// 面状控件:chip 描边 / slider 未激活轨 / switch 轨道 = 0.12f
assert!(
(surface_alpha - 0.12).abs() < f32::EPSILON,
"面状控件禁用 α 必须是 0.12(官方 FlatDisabledOutlineOpacity / \
DisabledInactiveTrackOpacity / DisabledTrackOpacity)"
);
assert!((content_alpha - 0.38).abs() < f32::EPSILON);
assert!(
surface_alpha > button_alpha,
"两族必须可区分:面状控件比按钮容器更实({surface_alpha} > {button_alpha})"
);
}
/// **面状控件族**的那一对颜色 = 0.12 预混合(chip / switch / slider / TextField 描边)。
///
/// ⚠️ 期望值**写死 0.12 / 0.38**,不从 `DISABLED_SURFACE_ALPHA` 取:
/// 从被测常量求期望是**自指断言**——常量一改,expected 跟着改,永远相等(复审 r1 P2-1 实测:
/// 把 `DISABLED_SURFACE_ALPHA` 改成 0.10,旧版这条照样绿)。现在常量改坏必红。
#[test]
fn surface_family_disabled_colors_are_the_12_percent_class() {
const EXPECTED_SURFACE_ALPHA: f32 = 0.12;
const EXPECTED_CONTENT_ALPHA: f32 = 0.38;
let actual_alpha = DISABLED_SURFACE_ALPHA;
assert_eq!(
actual_alpha, EXPECTED_SURFACE_ALPHA,
"面状控件族 α 必须是官方 0.12f;改常量会让下面每一条合成色断言同时红"
);
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let ui = ui(flavor);
// 期望值按**品牌色板**(= `Ui` 注入的那一份),不是 tone 派生的 baseline。
let scheme = unlucky_design::color::BrandBoard::for_flavor(flavor).scheme();
let base = scheme.get(Role::Surface);
let expected_container = scheme
.get(Role::OnSurface)
.with_alpha(EXPECTED_SURFACE_ALPHA)
.over(base);
let (surface, surface_content) = disabled_colors(&ui, Base::CANVAS);
assert_eq!(
surface,
to_iced(expected_container),
"{flavor:?}:面状控件容器 = on-surface@12% 叠表面(期望值硬编码,不取自常量)"
);
assert_eq!(
surface_content,
to_iced(
scheme
.get(Role::OnSurfaceVariant)
.with_alpha(EXPECTED_CONTENT_ALPHA)
.over(expected_container)
),
"{flavor:?}:面状控件内容仍是 38%,只是叠的底不同"
);
assert_ne!(surface, ui.color(Role::Surface), "12% 叠上去必须能看出来");
assert_eq!(surface.a, 1.0, "禁用容器也是实色(CPU/GPU 一致)");
assert_eq!(surface_content.a, 1.0, "禁用内容也是实色");
// 内容叠在容器上 vs 叠在表面上,是两回事(probe 量的是前者)
assert_ne!(
surface_content,
to_iced(
scheme
.get(Role::OnSurfaceVariant)
.with_alpha(EXPECTED_CONTENT_ALPHA)
.over(base)
),
"{flavor:?}:内容必须叠在容器上"
);
// 跨族对照:面状族的合成色必须与按钮族不同(两族一旦合流,这条红)
let (button, _) = disabled_button_colors(&ui, Base::CANVAS);
assert_ne!(
surface, button,
"{flavor:?}:面状族 12% 与按钮族 10% 必须落在不同像素值上"
);
}
}
/// **按钮家族**的那一对颜色 = 0.10 预混合,且与面状控件族**必须落在不同像素值上**。
///
/// 期望值同样写死(0.10 / 0.38),理由见上一条:自指断言抓不住常量被改坏。
#[test]
fn button_family_disabled_colors_are_the_10_percent_class() {
const EXPECTED_BUTTON_ALPHA: f32 = 0.10;
const EXPECTED_CONTENT_ALPHA: f32 = 0.38;
let actual_alpha = DISABLED_CONTAINER_ALPHA;
assert_eq!(
actual_alpha, EXPECTED_BUTTON_ALPHA,
"按钮族 α 必须是官方 0.1f(*ButtonTokens.DisabledContainerOpacity)"
);
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let ui = ui(flavor);
// 期望值按**品牌色板**(= `Ui` 注入的那一份),不是 tone 派生的 baseline。
let scheme = unlucky_design::color::BrandBoard::for_flavor(flavor).scheme();
let base = scheme.get(Role::Surface);
let expected_container = scheme
.get(Role::OnSurface)
.with_alpha(EXPECTED_BUTTON_ALPHA)
.over(base);
let (button, button_content) = disabled_button_colors(&ui, Base::CANVAS);
assert_eq!(
button,
to_iced(expected_container),
"{flavor:?}:按钮容器 = on-surface@10% 叠表面(官方 DisabledContainerOpacity = 0.1f)"
);
assert_eq!(
button_content,
to_iced(
scheme
.get(Role::OnSurfaceVariant)
.with_alpha(EXPECTED_CONTENT_ALPHA)
.over(expected_container)
),
);
let (surface, _) = disabled_colors(&ui, Base::CANVAS);
assert_ne!(
button, surface,
"{flavor:?}:10% 与 12% 必须落在不同像素值上(合成一个数就是这次要防的回归)"
);
// 内容色同理:按钮 38% 叠 10% 底 ≠ 面状 38% 叠 12% 底
assert_ne!(
button_content,
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
"{flavor:?}:两族的「有容器」内容色不能同源"
);
assert_eq!(
disabled_button_content(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
button_content,
"{flavor:?}:按钮内容入口必须与按钮颜色入口同源"
);
// 无容器时两族完全一致(都叠 surface)——差异只在「有容器」那一支
assert_eq!(
disabled_button_content(&ui, Base::CANVAS, Role::OnSurfaceVariant, false),
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, false),
"{flavor:?}:没有容器时不该有族差异"
);
}
}
/// **M229 复审 F1 的回归门**:禁用态的合成必须跟着**品牌色板**走,不许偷偷回到 tone 派生的 baseline。
///
/// Ink 板的中性色是绿系(`on-surface` 与 `surface` 的 G 通道最高),baseline 是蓝系;所以只要
/// 「禁用容器」在 Ink 下是绿系(`g >= b`)且与 baseline 算出来的值**不相等**,就说明它走的是板。
///
/// 抓得住:有人把三处 `Scheme::baseline(ui.flavor())` 写回去(那时本测试立刻红,且报出偏蓝的像素)。
/// 抓不住:色板本身改了色相(那是 `design` 的 `anchors_pin_the_spec` 的事)。
#[test]
fn disabled_colors_follow_the_brand_board_not_the_tone_baseline() {
use unlucky_design::color::BrandBoard;
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let ui = ui(flavor);
let board = BrandBoard::for_flavor(flavor).scheme();
let baseline = Scheme::baseline(flavor);
// ① 面状控件族的容器(chip 描边 / slider 未激活轨 / switch 轨道)
let (surface, _surface_content) = disabled_colors(&ui, Base::CANVAS);
let expect_surface = to_iced(
board
.get(Role::OnSurface)
.with_alpha(DISABLED_SURFACE_ALPHA)
.over(board.get(Role::Surface)),
);
assert_eq!(
surface, expect_surface,
"{flavor:?}:禁用容器必须按色板合成"
);
let baseline_surface = to_iced(
baseline
.get(Role::OnSurface)
.with_alpha(DISABLED_SURFACE_ALPHA)
.over(baseline.get(Role::Surface)),
);
assert_ne!(
surface, baseline_surface,
"{flavor:?}:禁用容器退回了 baseline(这就是复审 F1 的蓝系像素)"
);
// ② 按钮族的容器
let (button, _) = disabled_button_colors(&ui, Base::CANVAS);
assert_ne!(button, surface, "{flavor:?}:两族 α 仍要可区分");
// ③ 内容色(两个入口)
assert_eq!(
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
to_iced(
board
.get(Role::OnSurfaceVariant)
.with_alpha(DISABLED_CONTENT_ALPHA)
.over(
board
.get(Role::OnSurface)
.with_alpha(DISABLED_SURFACE_ALPHA)
.over(board.get(Role::Surface))
)
),
"{flavor:?}:面状控件族内容色要按色板合成"
);
assert_eq!(
disabled_button_content(&ui, Base::CANVAS, Role::OnSurfaceVariant, true),
to_iced(
board
.get(Role::OnSurfaceVariant)
.with_alpha(DISABLED_CONTENT_ALPHA)
.over(
board
.get(Role::OnSurface)
.with_alpha(DISABLED_CONTAINER_ALPHA)
.over(board.get(Role::Surface))
)
),
"{flavor:?}:按钮族内容色要按色板合成"
);
// ④ **色相判据**(像素级的自证):Ink 板的禁用容器是绿系(G 最高),baseline 是蓝系(B 最高)。
// Ink 板:`on-surface` 与 `surface` 的 G 通道都高于 B 通道(两个中性角色都是绿黑),
// 合成后仍是 G ≥ B;baseline 深色则是 B > G(天蓝中性)。浅色板上两者都是近中性,
// 所以只在深色档下判色相。
if matches!(flavor, SchemeFlavor::Dark) {
let (r, g, b) = channels(surface);
assert!(
g >= b,
"{flavor:?}:禁用容器 {r}/{g}/{b} 仍是蓝系(G < B),说明没走 Ink 板"
);
let (_, bg, bb) = channels(baseline_surface);
assert!(bb > bg, "对照组不合格:baseline 深色的禁用容器应当是蓝系");
}
}
}
/// **M234 的主判据**:禁用态的合成必须用**给的底**,不是画布色。
///
/// 卡片(`SurfaceContainer`)与面板(`SurfaceContainerLow`)里各放一个禁用控件时,
/// 10%(按钮族)/ 12%(面状族)那一层要叠在**卡/面板的底**上;叠在 `Surface` 上就是 M223
/// 记的「CR 1.05、形丢失」。
///
/// 抓得住:① 入口把 `base` 丢了(内部又写死 `Role::Surface`);② 入口把两个族的 α 合成一个数;
/// ③ 调用点给了画布底(这条测试只证「给了卡片底 → 算出卡片色」,调用点是否真的给了由
/// `buttons::tests::a_disabled_button_composites_over_the_base_it_is_given` 与 gallery 的像素
/// probe 分别守)。
///
/// 抓不住:① 容器画的是**合成色**(半透明面板 α、叠过状态层的底)——`Base` 只装一个 `Role`;
/// ② 卡/面板组件**画底时**用的角色与它导出的 `BASE` 不一致(`BASE` 的值本身由
/// `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints` 钉住,
/// 「card() 真的用了它」与「页面里的调用点传了它对不对」由 `gallery::probe` 的 disabledbase
/// 段守);③ GPU(wgpu) 档(本测试是纯值,两档同源同值)。
#[test]
fn disabled_paint_composites_over_the_base_it_is_given() {
use unlucky_design::color::BrandBoard;
use unlucky_design::color::Role;
const BUTTON_ALPHA: f32 = 0.10;
const SURFACE_ALPHA: f32 = 0.12;
const CONTENT_ALPHA: f32 = 0.38;
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let ui = ui(flavor);
let scheme = BrandBoard::for_flavor(flavor).scheme();
let card = Base::of(Role::SurfaceContainer);
let panel = Base::of(Role::SurfaceContainerLow);
// 期望值一律**独立算**(写死 α + 直接取色板角色),不调被测常量。
let over = |base: Role, alpha: f32| {
to_iced(
scheme
.get(Role::OnSurface)
.with_alpha(alpha)
.over(scheme.get(base)),
)
};
// ① 按钮族 10%:卡片底 / 面板底 / 画布底三档互不相等
let (card_button, card_button_ink) = disabled_button_colors(&ui, card);
let (panel_button, _) = disabled_button_colors(&ui, panel);
let (canvas_button, canvas_button_ink) = disabled_button_colors(&ui, Base::CANVAS);
assert_eq!(
card_button,
over(Role::SurfaceContainer, BUTTON_ALPHA),
"{flavor:?}:卡片里的禁用按钮容器 = on-surface@10% 叠**卡片底**"
);
assert_eq!(
panel_button,
over(Role::SurfaceContainerLow, BUTTON_ALPHA),
"{flavor:?}:面板里的禁用按钮容器 = on-surface@10% 叠**面板底**"
);
assert_ne!(card_button, canvas_button, "{flavor:?}:卡片底 ≠ 画布底");
assert_ne!(panel_button, canvas_button, "{flavor:?}:面板底 ≠ 画布底");
assert_ne!(card_button, panel_button, "{flavor:?}:卡片底 ≠ 面板底");
// ② 面状控件族 12%(字段容器 / chip 容器 / switch 轨道)
let (card_surface, _) = disabled_colors(&ui, card);
let (panel_surface, _) = disabled_colors(&ui, panel);
let (canvas_surface, _) = disabled_colors(&ui, Base::CANVAS);
assert_eq!(
card_surface,
over(Role::SurfaceContainer, SURFACE_ALPHA),
"{flavor:?}:卡片里的禁用字段容器 = on-surface@12% 叠**卡片底**"
);
assert_eq!(
panel_surface,
over(Role::SurfaceContainerLow, SURFACE_ALPHA),
"{flavor:?}:面板里的禁用字段容器 = on-surface@12% 叠**面板底**"
);
assert_ne!(card_surface, canvas_surface);
assert_ne!(panel_surface, canvas_surface);
// ③ 内容色也跟着底走(38% 叠在容器上 / 直接叠在底上,两支都要换底)
assert_eq!(
disabled_button_content(&ui, card, Role::OnSurfaceVariant, true),
to_iced(
scheme
.get(Role::OnSurfaceVariant)
.with_alpha(CONTENT_ALPHA)
.over(
scheme
.get(Role::OnSurface)
.with_alpha(BUTTON_ALPHA)
.over(scheme.get(Role::SurfaceContainer))
)
),
"{flavor:?}:有容器的内容色 = 38% 叠(10% 叠卡片底)"
);
assert_ne!(
card_button_ink, canvas_button_ink,
"{flavor:?}:内容叠的容器换了底"
);
assert_ne!(
disabled_content_over_role(&ui, card, Role::OnSurface, false),
disabled_content_over_role(&ui, Base::CANVAS, Role::OnSurface, false),
"{flavor:?}:无容器时 38% 也必须叠在**给的底**上"
);
}
}
/// iced 颜色 → 8bit 通道(像素值,报告里的证据就用它)。
fn channels(color: Color) -> (u8, u8, u8) {
(
(color.r * 255.0).round() as u8,
(color.g * 255.0).round() as u8,
(color.b * 255.0).round() as u8,
)
}