Files
Unlucky-Launcher/crates/design/src/typography/fonts/tests.rs
T
AskaEth 83e19e58f0 fix(design): M61 r1 收口 —— 官方 emphasized/弹簧/高程 + 冻结测试
reviewer r1 判 p2-2items(fix-then-merge),本轮按条目收口:

P2-1 冻结测试补全(变异实测证明原来可静默漂移):
- tests/type_scale_pins.rs:baseline 15 档 (size,line_height,weight,tracking,family)
  与 Expressive 强调 15 档逐值对拍,期望值独立于实现重写
- tests/token_pins.rs:49 条 tone(light+dark 全表)、7 条 easing 曲线参数、
  16 档时长、9 组弹簧、28 个 IconKind 下标、形状阶梯
- 自证:TitleMedium 16→17 与 EmphasizedDecelerate 换曲线两次变异均 FAILED

P2-2 emphasized 口径事实错误(用户点名):
- 取回 A 级官方值(androidx TypeScaleTokens v0_103 的 *Emphasized* 四元组):
  label-*/title-medium/title-small 强调是 700(此前给 600)、body-large 0.5→0.15、
  title-medium 0.2→0.15、body-medium 0.2→0.25、display-large -0.2→0
- TypeRole::emphasized() 改为官方表(typography/emphasized.rs);
  原「只加一档字重」规则改名 emphasized_simplified() 并标注非官方
- 顺带按官方订正 baseline tracking(display-large -0.2、body-medium 0.2、
  title-medium 0.2)与 TitleLarge 的 Brand 族归属
- crate 文档新增「baseline MD3 vs M3 Expressive 差异清单」+ 未支持能力 gap

弹簧改为两套 token 结构(用户追加令):
- motion::spring(scheme × category × speed) 二维表 12 格:
  standard spatial ζ=0.9 (k 1400/700/300)、expressive spatial ζ=0.6~0.8 (k 800/380/200)、
  effects 两套同值 ζ=1.0 (k 3800/1600/800),均取 A 级 MotionTokens 原值
- Spring 改存阻尼比(与 Compose 令牌同口径),9 个具名常量

中文字重覆盖(用户追加令):
- FontFace 带 WeightCoverage(可变区间/静态表),Platform::resolve_weight 给
  Native/Nearest/Synthetic 三档结论;雅黑无 500、苹方无 700 两处 gap 显式登记

component_map 高程按官方回调(P2 并入 finding):
- FilledCard L2→L0、OutlinedCard L1→L0、Menu L4→L2、Dialog L5→L3(Menu 圆角同时
  按 MenuTokens 改 4dp);新增官方值 pin 测试 + R4「全屏都在飘」守门断言

P3 四条:
- 同族容器撞色:彩度按共享基准等比压缩(Palettes.reference_chroma),
  primary/secondary-container 不再同值,且强调色覆盖时表面逐像素不变
- femtovg 引用改函数名(行号已漂);删掉 contrast_aa 的死代码后门
- 骨架数值跟 M63:nav 56→44、titlebar 40→48,control 40/touch 48 换算依据入文档
- 图标折算口径订正为「居中描边,视觉外扩 <1px」
2026-11-01 15:58:19 +08:00

150 lines
5.2 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.
//! 字体栈与字重解析的测试(独立文件是为了守住单文件 ≤400 行的硬约束)。
use super::*;
use crate::typography::Weight;
const PLATFORMS: [Platform; 3] = [Platform::Windows, Platform::MacOs, Platform::Linux];
const FAMILIES: [FontFamily; 3] = [FontFamily::Display, FontFamily::Text, FontFamily::Mono];
#[test]
fn font_stacks_are_well_formed() {
for platform in PLATFORMS {
for family in FAMILIES {
let stack = platform.font_stack(family);
assert!(
!stack.is_empty(),
"{} 的 {family:?} 栈为空",
platform.name()
);
let last = stack[stack.len() - 1].family;
let generic = match family {
FontFamily::Mono => "monospace",
_ => "sans-serif",
};
assert_eq!(
last,
generic,
"{} 的 {family:?} 栈必须以通用族名兜底",
platform.name()
);
for face in stack {
assert!(!face.family.is_empty());
assert!(
!face.family.contains(['/', '\\', '.', ':']),
"字体栈只允许族名,不得出现路径/文件名:{}",
face.family
);
assert!(
face.family.is_ascii(),
"族名应为 ASCII(非 ASCII 名交给系统匹配):{}",
face.family
);
// 每条栈项都必须声明字重覆盖(连 400 都没有的族写进栈没有意义)。
assert!(
face.supports(Weight::Regular),
"{} 连 400 都没有,写它没意义",
face.family
);
}
}
}
}
#[test]
fn display_and_text_stacks_differ_on_windows_and_macos() {
assert_ne!(
Platform::Windows.font_stack(FontFamily::Display)[0].family,
Platform::Windows.font_stack(FontFamily::Text)[0].family
);
assert_ne!(
Platform::MacOs.font_stack(FontFamily::Display)[0].family,
Platform::MacOs.font_stack(FontFamily::Text)[0].family
);
}
#[test]
fn chinese_fallback_is_present_but_not_first() {
// 中文族名必须在栈里,但不能排第一(纯英文环境不加载中文字体,D28)。
for platform in PLATFORMS {
let stack = platform.font_stack(FontFamily::Text);
let cjk = ["Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC"];
assert!(
stack.iter().any(|face| cjk.contains(&face.family)),
"{} 的正文栈缺 CJK 兜底",
platform.name()
);
assert!(
!cjk.contains(&stack[0].family),
"{} 的首选字体不该是 CJK 族",
platform.name()
);
}
}
#[test]
fn cjk_weight_coverage_is_registered() {
// 用户点名的坑:中文字重覆盖三平台差别很大。
let win = Platform::Windows.cjk_face();
assert!(win.supports(Weight::Regular) && win.supports(Weight::Bold));
assert!(
!win.supports(Weight::Medium),
"雅黑没有 500——这条断言就是「中文 500 会掉到降级路径」的登记"
);
let mac = Platform::MacOs.cjk_face();
assert!(mac.supports(Weight::Medium));
assert!(
!mac.supports(Weight::Bold),
"苹方到 600 为止,700 要合成/换族"
);
let linux = Platform::Linux.cjk_face();
for weight in [Weight::Regular, Weight::Medium, Weight::Bold] {
assert!(
linux.supports(weight),
"思源黑体/Noto CJK 应覆盖 400/500/700"
);
}
}
#[test]
fn resolve_prefers_native_and_flags_synthetic() {
// Windows 正文 700:Segoe UI Variable 覆盖 → 原生。
let bold = Platform::Windows.resolve_weight(FontFamily::Text, Weight::Bold);
assert_eq!(bold.quality, WeightMatch::Native);
assert_eq!(bold.available, 700);
assert!(!bold.is_synthetic());
// Linux 正文 500:Roboto 可变 → 原生。
assert_eq!(
Platform::Linux
.resolve_weight(FontFamily::Text, Weight::Medium)
.quality,
WeightMatch::Native
);
// 静态族缺档:只有 400 的族请求 700 → nearest 400,差 300 → 判为合成降级。
let only_regular = FontFace::new("Courier New", WeightCoverage::Static(&[400]));
assert_eq!(only_regular.weights.nearest(700), 400);
let synthetic = FontChoice {
family: only_regular.family,
requested: 700,
available: only_regular.weights.nearest(700),
quality: WeightMatch::Synthetic,
};
assert!(synthetic.is_synthetic());
}
#[test]
fn nearest_picks_closest_static_weight() {
let coverage = WeightCoverage::Static(&[300, 400, 600, 700]);
assert_eq!(coverage.nearest(500), 400);
assert_eq!(coverage.nearest(650), 600);
assert_eq!(coverage.nearest(900), 700);
let variable = WeightCoverage::Variable { min: 100, max: 900 };
assert_eq!(variable.nearest(500), 500);
assert_eq!(variable.nearest(50), 100);
}
#[test]
fn current_platform_returns_a_stack() {
let platform = Platform::current();
assert!(!platform.font_stack(FontFamily::Text).is_empty());
}