复审 p1:`detect_env()` 在 Windows 上恒为 `en`(`LC_ALL/LC_MESSAGES/LANG` 都不存在), D28 整条中文链(探测 → 扫 CJK → 需下载)在基线平台永不触发。复审在本机(zh-CN)实测 `lang=en raw=None`,这条我认。 **p1 修法(tower 定路线:子进程读注册表,不碰 FFI、不加依赖)** - 新增 `crates/app-iced/src/platform/locale.rs`:`SystemLocale` 实现 `unlucky_i18n::lang::LocaleSource`,内部 `reg.exe query "HKCU\Control Panel\International" /v LocaleName`(本机实测 `REG_SZ zh-CN`),读不到时退 `/v Locale` 的十六进制 LCID (本机 `00000804`),用一张 20 条的短表翻成标签,表外语言返回 `None`(宁可英文,不猜语种)。 先例同 M73 壁纸探测(`core::platform::wallpaper` 的 `reg.exe` 路线)。 - 子进程走注入的 `RegIo` 接口,单测用 `FakeReg`(不 spawn);夹具是**本机真机抓的原文** (含 CRLF / 前导空白),另有一条 `#[cfg(windows)]` 真机测试证明 `reg.exe` 这条路真的通。 - i18n 仍是纯函数:`lang::resolve(setting, platform, env)` 是四级优先级的唯一实现—— **`ui.language` 显式值 > 平台源 > POSIX 环境变量 > 英文**,`Detection` 新增 `origin` (`setting` / 源自报的 `label()` / `env` / `default`),启动日志因此能写清 `source=registry`。 配置值不认识时**跳过**继续往下走,不当成任何一种语言。 - `fonts::cjk::probe_with(setting, faces)` 把 `ui.language` 那一级的位置先留好(目前传 `None`: 设置层还没有读取点,写在模块文档的诚实边界里);`probe()` 走全链。 - `platform::mod` 的模块表补一行说明 locale 也是「零 unsafe」那一类。 **本机(zh-CN)实跑证据**(`--software-renderer --screenshot …`,exit 0): `[font] … | lang=zh-CN locale=zh-CN source=registry cjk=system:"Microsoft YaHei UI"` ——探测出中文、且走的是「扫 CJK 字体」而不是 `not-needed`。 **p2**:删掉 `catalog` 那条 `compile_fail` 守卫(复审两个变异仍全绿 ⇒ 它守的是「这段代码 编译不过」,不是「编译期校验还在」)。改为陈述事实:「`Key` 是枚举,拼错 `E0599`」, 并写明这里**没有**守卫测试及原因。 未动 `ui.language` 默认值(`core` schema 不在本 mission scope,tower 已另记)。
176 lines
5.2 KiB
Rust
176 lines
5.2 KiB
Rust
//! `fonts` 的单元测试(拆出来只为「src 文件 ≤400 行」的门禁)。
|
||
|
||
use super::cjk::CjkReport;
|
||
use super::*;
|
||
use unlucky_design::typography::WeightCoverage;
|
||
use unlucky_i18n::font::Strategy;
|
||
use unlucky_i18n::lang::Detection;
|
||
|
||
fn only(installed: &'static [&'static str]) -> impl Fn(&str) -> bool {
|
||
move |family| installed.contains(&family)
|
||
}
|
||
|
||
#[test]
|
||
fn windows_stack_falls_back_to_classic_segoe_when_variable_is_absent() {
|
||
// Win10 没有 Segoe UI Variable Text
|
||
let family = pick(
|
||
Platform::Windows,
|
||
FontFamily::Text,
|
||
&only(&["Segoe UI", "Microsoft YaHei UI"]),
|
||
);
|
||
assert_eq!(family, "Segoe UI");
|
||
}
|
||
|
||
#[test]
|
||
fn windows_stack_prefers_the_variable_face_on_win11() {
|
||
let family = pick(
|
||
Platform::Windows,
|
||
FontFamily::Text,
|
||
&only(&["Segoe UI Variable Text", "Segoe UI"]),
|
||
);
|
||
assert_eq!(family, "Segoe UI Variable Text");
|
||
}
|
||
|
||
#[test]
|
||
fn generic_family_names_are_never_reported_as_the_resolved_family() {
|
||
// 一个都没装:返回栈头,且栈头不是通用族名
|
||
let family = pick(Platform::Linux, FontFamily::Text, &only(&[]));
|
||
assert_eq!(family, "Roboto");
|
||
assert!(!is_generic(family));
|
||
}
|
||
|
||
#[test]
|
||
fn missing_cjk_fonts_do_not_change_the_latin_pick() {
|
||
// 装了苹方但没装 SF:macOS 栈应该选 Helvetica Neue 而不是中文族
|
||
let family = pick(
|
||
Platform::MacOs,
|
||
FontFamily::Text,
|
||
&only(&["PingFang SC", "Helvetica Neue"]),
|
||
);
|
||
assert_eq!(family, "Helvetica Neue");
|
||
}
|
||
|
||
#[test]
|
||
fn weight_reports_use_the_token_degradation_path() {
|
||
// 没有系统字面表时回落到令牌纯函数:Windows 栈里 Segoe UI Variable Text 是可变 300..700
|
||
let medium = weight_report(
|
||
Platform::Windows,
|
||
"Segoe UI Variable Text",
|
||
Weight::Medium,
|
||
None,
|
||
);
|
||
assert_eq!(medium.requested, 500);
|
||
assert_eq!(medium.quality, WeightMatch::Native);
|
||
|
||
// 手工构造一个「只有 400/700」的族(= 经典 Segoe UI),500 应该落到最近档
|
||
let faces = [
|
||
Face {
|
||
family: "Segoe UI".to_owned(),
|
||
weight: 400,
|
||
},
|
||
Face {
|
||
family: "Segoe UI".to_owned(),
|
||
weight: 700,
|
||
},
|
||
];
|
||
let medium = weight_report(Platform::Windows, "Segoe UI", Weight::Medium, Some(&faces));
|
||
assert_eq!(medium.available, 400, "400 比 700 更近");
|
||
assert_eq!(medium.quality, WeightMatch::Nearest);
|
||
|
||
// 只有 400 却要 700 → 合成加粗
|
||
let only_regular = [Face {
|
||
family: "Narrow".to_owned(),
|
||
weight: 400,
|
||
}];
|
||
let bold = weight_report(
|
||
Platform::Windows,
|
||
"Narrow",
|
||
Weight::Bold,
|
||
Some(&only_regular),
|
||
);
|
||
assert_eq!(bold.quality, WeightMatch::Synthetic);
|
||
|
||
// 原生有 500 就用 500
|
||
let has_medium = [Face {
|
||
family: "Noto".to_owned(),
|
||
weight: 500,
|
||
}];
|
||
assert_eq!(
|
||
weight_report(Platform::Linux, "Noto", Weight::Medium, Some(&has_medium)).quality,
|
||
WeightMatch::Native
|
||
);
|
||
|
||
let narrow = WeightCoverage::Static(&[400]);
|
||
assert_eq!(narrow.nearest(700), 400);
|
||
}
|
||
|
||
#[test]
|
||
fn describe_is_one_line_and_mentions_all_three_stacks() {
|
||
let resolved = Resolved {
|
||
platform: Platform::Windows,
|
||
families: [
|
||
FamilyReport {
|
||
stack: "text",
|
||
family: "Segoe UI",
|
||
installed: true,
|
||
},
|
||
FamilyReport {
|
||
stack: "display",
|
||
family: "Segoe UI Variable Display",
|
||
installed: false,
|
||
},
|
||
FamilyReport {
|
||
stack: "mono",
|
||
family: "Cascadia Mono",
|
||
installed: true,
|
||
},
|
||
],
|
||
weights: [
|
||
WeightReport {
|
||
requested: 400,
|
||
available: 400,
|
||
quality: WeightMatch::Native,
|
||
},
|
||
WeightReport {
|
||
requested: 500,
|
||
available: 400,
|
||
quality: WeightMatch::Nearest,
|
||
},
|
||
WeightReport {
|
||
requested: 700,
|
||
available: 400,
|
||
quality: WeightMatch::Synthetic,
|
||
},
|
||
],
|
||
cjk: CjkReport {
|
||
detection: Detection {
|
||
lang: unlucky_i18n::Lang::En,
|
||
raw: None,
|
||
origin: unlucky_i18n::lang::ORIGIN_DEFAULT,
|
||
},
|
||
strategy: Strategy::NotNeeded,
|
||
},
|
||
};
|
||
|
||
let line = resolved.describe();
|
||
assert_eq!(line.lines().count(), 1);
|
||
assert!(line.contains("text=\"Segoe UI\""), "{line}");
|
||
assert!(line.contains("未装机"), "{line}");
|
||
assert!(line.contains("w700=400(synthetic)"), "{line}");
|
||
assert!(line.contains("cjk=not-needed"), "英文环境要进日志:{line}");
|
||
assert!(resolved.has_synthetic());
|
||
assert_eq!(resolved.families().text, "Segoe UI");
|
||
}
|
||
|
||
#[test]
|
||
fn type_roles_pick_the_display_stack_for_big_text() {
|
||
use unlucky_design::typography::TypeRole;
|
||
|
||
assert_eq!(
|
||
TypeRole::TitleLarge.style().family,
|
||
FontFamily::Display,
|
||
"22px 标题走 Display 栈"
|
||
);
|
||
assert_eq!(TypeRole::BodyMedium.style().family, FontFamily::Text);
|
||
}
|