Files
Unlucky-Launcher/crates/app-iced/src/fonts/tests.rs
T
AskaEth ce2c42032b fix(i18n): M92 r1 —— Windows 真 locale 走注册表子进程(零 FFI)+ 删掉假守卫
复审 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 已另记)。
2026-11-02 16:58:09 +08:00

176 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.
//! `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);
}