Files
Unlucky-Launcher/crates/app-iced/src/components/feedback/tests.rs
T
AskaEth c723d1faff fix(iced/feedback): 空态间距双算、图标块对比度、Dialog 宽度与动作禁用底色(P2-1/2/4)
- P2-1 空态间距双算:`Column::spacing(8)` 与显式 `gap(16/24)` 叠加 ⇒ 实际渲染 32/40、
  条目总高 264,而登记的是 16/8/24、232。现在 `spacing(0.0)`,间距只有一个来源(`gap()`);
  断言从 `gap > 0.0` 改成**精确等于令牌档位** 16/8/24。
- P2-2 空态图标块几乎没有边界:`SurfaceContainerLow` 与画布 ΔRGB 只有 11/5/0(≈1%),
  浅色下看不出块。换 `SurfaceContainerHigh`(材质层 Overlay)+ 1px `OutlineVariant` 描边,
  并用 `block_layer()` 让 probe 与组件取同一个层(不再各写一份角色)。
- P2-1 Dialog 恒 480 宽:`Length::Fixed(480)` 在窄宿主会溢出,`.max_width(560)` 对 Fixed 是
  空操作。改成 `Fill + max_width(480)` = `min(宿主, 480)`(iced 无 min-width,见 G17-d)。
- P2-4 动作禁用底色取错:`interaction::disabled_content_over_role(..., false)` 把 38% 内容色
  固定叠在 **Surface** 上,而 Snackbar 的底是 `InverseSurface`、Dialog 是 `SurfaceContainerHigh`
  ⇒ 真出现禁用态会是「38% 深字叠深底」。改为 `content_color()` 按真实容器角色合成(同令牌 α)。
- probe:Dialog 上内距判据 `(24..=40)` 收紧到 `(24..=30)`;空态新增「块↔标题 ≈16」与
  「条目总高 ≈232」两条渲染层断言。
2026-11-02 15:56:44 +08:00

194 lines
7.7 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.
//! 纯函数级断言:几何对令牌、节奏对 A1、角色对变体。
//!
//! 这里**只测数值与结构**(M68 的教训:函数对不代表图对)。对着真截图量像素的是
//! [`super::probe`],两者缺一不可——本文件保证「数字对」,probe 保证「画出来是那个数字」。
use iced::Element;
use unlucky_design::color::{Role, Scheme, SchemeFlavor, SCRIM_ALPHA};
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::typography::Platform;
use crate::theme::{to_iced, Families, Ui};
use super::super::interaction::{self, Interaction};
use super::dialog::scrim;
use super::panel::{DialogGeometry, EmptyStateGeometry, SnackbarGeometry};
use super::snackbar::{Placement, Snackbar};
use super::{circular, dialog, empty_state, linear, snackbar, Dialog, EmptyState};
mod progress;
const FAMILIES: Families = Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
};
pub(super) fn ui(flavor: SchemeFlavor) -> Ui {
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
}
// ---- §14 Snackbar -----------------------------------------------------------
/// 14.3 / 14.1 / 宽度上限:48 / 68 / 4 / 600。
#[test]
fn snackbar_heights_widths_and_radius_are_the_tokens() {
let geometry = SnackbarGeometry::of(&ui(SchemeFlavor::Light));
assert_eq!(geometry.height_for(1), 48.0, "14.3 单行 48");
assert_eq!(geometry.height_for(2), 68.0, "14.3 双行 68");
assert_eq!(geometry.radius, 4.0, "14.1 圆角 4(CornerExtraSmall)");
assert_eq!(geometry.max_width, 600.0, "SnackbarKt.ContainerMaxWidth");
assert_eq!(geometry.min_width, 344.0, "G17-d [估] 最小宽");
assert!(geometry.min_width < geometry.max_width, "上下限必须有区间");
assert_eq!(geometry.margin, 12.0, "G17-c [规格] 外边距 12");
assert_eq!(geometry.pad_h, 16.0, "A1 HorizontalSpacing");
assert_eq!(geometry.pad_v, 6.0, "A1 SnackbarVerticalPadding");
}
// ---- §7 Dialog --------------------------------------------------------------
/// 7.4:官方区间 **280..560**,本项目取 **480**,且 480 落在区间内。
#[test]
fn dialog_geometry_sits_inside_the_official_bounds() {
let geometry = DialogGeometry::of(&ui(SchemeFlavor::Light));
assert_eq!(geometry.min_width, 280.0, "A1 DialogMinWidth");
assert_eq!(geometry.max_width, 560.0, "A1 DialogMaxWidth");
assert_eq!(geometry.width, 480.0, "G17-e 本项目取值");
assert!(
geometry.min_width <= geometry.width && geometry.width <= geometry.max_width,
"自选宽必须落在官方区间内"
);
}
/// 7.1 / 7.3 / 7.6:圆角 28、内距 24 / 16 / 24、动作 8 / 12。
#[test]
fn dialog_padding_and_action_gaps_are_the_tokens() {
let geometry = DialogGeometry::of(&ui(SchemeFlavor::Light));
assert_eq!(geometry.radius, 28.0, "7.1 CornerExtraLarge");
assert_eq!(geometry.padding, 24.0, "7.3 DialogPadding");
assert_eq!(geometry.title_gap, 16.0, "7.3 TitlePadding bottom");
assert_eq!(geometry.body_gap, 24.0, "7.3 TextPadding bottom");
assert_eq!(geometry.action_gap, 8.0, "7.6 ButtonsMainAxisSpacing");
assert_eq!(geometry.action_cross, 12.0, "7.6 ButtonsCrossAxisSpacing");
}
/// 7.7 + D27:scrim = 黑@0.32 叠在 canvas 上之后的**不透明**实色。
#[test]
fn scrim_is_scrimalpha_precomposited_onto_the_canvas() {
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
let ui = ui(flavor);
let scheme = Scheme::baseline(flavor);
let canvas = scheme.get(MaterialLayer::Canvas.surface_role());
let expected = to_iced(scheme.scrim().over(canvas));
assert_eq!(
scrim(&ui),
expected,
"{flavor:?}:scrim 必须逐位等于「黑@0.32 叠 canvas」"
);
assert_eq!(
scrim(&ui).a,
1.0,
"D27:CPU 档不出现半透明(预合成后必须不透明)"
);
assert_ne!(
scrim(&ui),
ui.layer(MaterialLayer::Canvas),
"遮罩必须与画布有可见差异"
);
assert!((SCRIM_ALPHA - 0.32).abs() < f32::EPSILON, "令牌口径 α=0.32");
}
}
// ---- §15 空态(项目自定) ----------------------------------------------------
/// 15.1:图标块 96 / 圆角 16 / 图标 32(**全部 `[估]`**,官方没有这个组件)。
#[test]
fn empty_state_geometry_is_the_project_defined_set() {
let geometry = EmptyStateGeometry::of(&ui(SchemeFlavor::Light));
assert_eq!(geometry.block, 96.0);
assert_eq!(geometry.block_radius, 16.0, "ShapeScale::Large");
assert_eq!(geometry.icon, 32.0, "IconSize::Empty");
// **复审 P2-1 的回归断言**:三个间距必须精确等于令牌档位。
// 早先这里只写 `> 0.0`,所以「Column::spacing(8) 与显式 gap(16/24) 双算成 32/40」
// 完全藏得住(渲染实测 264 vs 登记 232)。
assert_eq!(geometry.gap_title, 16.0, "块↔标题 = Space::X4");
assert_eq!(geometry.gap_body, 8.0, "标题↔副文案 = Space::X2");
assert_eq!(geometry.gap_action, 24.0, "副文案↔CTA = Space::X6");
assert_eq!(geometry.block, 96.0);
assert_eq!(super::empty_state::block_layer(), MaterialLayer::Overlay);
}
// ---- 结构 / 角色 ------------------------------------------------------------
/// 五个入口都能在**任意**消息类型下构建(契约 §2:不 import `app::Message`)。
#[test]
fn every_component_builds_for_a_foreign_message_type() {
#[derive(Clone, Debug, PartialEq)]
enum Other {
Yes,
No,
}
let ui = ui(SchemeFlavor::Light);
let _: Element<'_, Other> = linear(&ui, Progress::determinate(0.5));
let _: Element<'_, Other> = circular(&ui, Progress::indeterminate(0.3));
let _: Element<'_, Other> = snackbar(
&ui,
Snackbar::new("已保存")
.placement(Placement::Top)
.two_lines()
.action("撤销", Other::No)
.dismiss(Other::No),
);
let _: Element<'_, Other> = dialog(
&ui,
Dialog::new("确认删除?", "此操作不可撤销。")
.double(("取消", Other::No), ("删除", Other::Yes)),
);
let _: Element<'_, Other> = empty_state(
&ui,
EmptyState::new(
unlucky_design::icons::IconKind::Instances,
"还没有实例",
"建一个开始游戏",
)
.action("新建", Other::Yes),
);
// 无动作的三种形态也要能构建(DialogActions::None / 无 action 的 Snackbar)
let _: Element<'_, Other> = dialog(&ui, Dialog::new("已是最新版本", "没有可用更新。"));
let _: Element<'_, Other> = snackbar(&ui, Snackbar::new("已复制"));
let _: Element<'_, Other> = empty_state(
&ui,
EmptyState::new(
unlucky_design::icons::IconKind::Logs,
"没有日志",
"启动一次游戏后这里会有内容",
),
);
}
/// 状态层必须来自 `interaction`(不是组件里自己乘 α):hover 层不透明且与容器色不同。
#[test]
fn snackbar_state_layers_come_from_the_interaction_module() {
let ui = ui(SchemeFlavor::Light);
let hover = interaction::overlay_color(
&ui,
Interaction::Hovered,
Role::InversePrimary,
Role::InverseSurface,
)
.expect("hover 有状态层");
assert_eq!(hover.a, 1.0, "状态层必须是实色(CPU/GPU 一致)");
assert_ne!(
hover,
ui.color(Role::InverseSurface),
"叠了 8% 必须看得出来"
);
// 禁用内容走 token 口径(38%),不是「变灰一点」
let disabled = interaction::disabled_content_over_role(&ui, Role::OnSurface, false);
assert_ne!(disabled, ui.color(Role::OnSurface));
assert_eq!(disabled.a, 1.0);
}