Files
Unlucky-Launcher/crates/app-iced/src/pages/settings/tests/general_section.rs
T
AskaEth 44a0c87a11
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
style: cargo fmt --all(合并期整合修复)
M187/M188 并入后全仓 fmt --check 为红(CI 的 fmt job 会红)。
本次仅 cargo fmt --all 的换行/缩进变化,无语义改动;
cargo fmt --check 与 cargo check 均已复验为绿。
2026-11-09 01:43:31 +08:00

248 lines
11 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.
//! 「General」分组自己的断言(M106 `I18NFOLLOW` 重写:语言行换成真控件)。
//!
//! **归 language+appearance 线**。接真控件那天要改**两个**文件(M101 r2 删掉了原先的第 3 处跨
//! section 绊线):`settings/general.rs` + 本文件 `the_general_group_is_still_demo_rows`。
//!
//! M106(`I18NFOLLOW`)已经接了这一组的语言行。用户报的 bug 有两半:**界面全英文**(探测对了
//! 但没人用)与**设置页写着 `English (System)`**(一行硬编码假值)。本文件的断言守后半:那行
//! 假值不许回来,语言行的三档必须真的能在两套目录里取到词。
//!
//! 接线形状:语言行的正文由 [`general::body`] 画(入口是 `pages::settings` 的 `GENERAL_GROUP`
//! 按下标分发),`GROUP.body` 仍是 `None`——那个槽位现在带**生效语言**(`Lang`,M179)但仍是
//! `States` + 语言,装不下「当前语言档位」(`LangChoice`)。所以 M101 r2 的
//! 「本组接真控件只需改两个文件」这条口径,在语言行上**仍然是两个文件**。
use super::super::general;
use unlucky_design::icons::IconKind;
use unlucky_i18n::LangChoice;
use unlucky_i18n::{Key, Lang};
/// 这一组的**静态演示行**:语言那一行已经换真控件,所以只剩三行。
///
/// 抓得住:①四条线并行合并时把别人的行粘到本文件;②语言行被「顺手」加回静态表——
/// 那等于把用户看到的假值又写回去(它的值必须跟着语言变,静态表做不到)。
/// 抓不住:真控件画出来长什么样(那是截图验收的事,见 `DevDocs/artifacts/m106/`)。
#[test]
fn the_general_group_keeps_theme_startup_and_close_as_static_rows() {
let rows: Vec<(&str, &str)> = general::GROUP.rows.to_vec();
assert_eq!(
rows,
vec![
("Theme", "Follow system"),
("Startup page", "Instances"),
("Close button", "Exit to tray"),
]
);
}
/// **M168(D28 后半)**:中文字体下载状态行的四种状态都得有话说,且两套目录都不许 `⟪missing⟫`。
///
/// 抓得住:①只画「下载成功」一种(失败的用户会以为程序坏了);②失败态的文案写成「保持英文」
/// (D28 的实际行为是**继续用系统字体**,界面语言不变);③占位符没被替换(用户会看到 `{family}`);
/// ④新 key 只进了英文表(中文界面上出现 `⟪missing⟫`)。
/// 抓不住:真下载时的样子(那要联网)、字体有没有真的出字(截图 + 真机,见报告)。
#[test]
fn the_font_status_row_speaks_in_four_states_and_two_languages() {
use crate::fonts::install::{CjkInstall, FetchClass};
let states = [
CjkInstall::Downloading { bytes: 17_772_300 },
CjkInstall::Ready {
family: "Noto Sans SC".to_owned(),
from_cache: true,
},
CjkInstall::Failed {
class: FetchClass::Network,
message: "timeout".to_owned(),
},
CjkInstall::System {
family: "Microsoft YaHei".to_owned(),
},
CjkInstall::NotNeeded,
CjkInstall::Unprobeable,
CjkInstall::Unknown,
];
for lang in Lang::ALL {
let catalog = lang.catalog();
assert!(
catalog.get(Key::FontCjkStatusLabel).is_some(),
"{} 目录里缺中文字体那一行的标签",
lang.tag()
);
for state in &states {
let (value, hint) = general::font_status(&catalog, state);
assert!(!value.is_empty(), "{state:?} 没话说");
assert!(!value.contains('{'), "{state:?} 占位符没替换:{value}");
assert!(
!value.contains(unlucky_i18n::catalog::MISSING),
"{state:?} 在 {} 目录里缺译:{value}",
lang.tag()
);
if let Some(hint) = hint {
assert!(!hint.contains('{'), "{state:?} 的提示没替换:{hint}");
}
}
}
// 失败态必须说清三类原因之一(而不是一句「失败了」)。
let zh = Lang::ZhCn.catalog();
for (class, reason) in [
(FetchClass::Network, "网络不通"),
(FetchClass::Integrity, "镜像内容校验不通过"),
(FetchClass::Disk, "写不进数据目录"),
] {
let (value, _) = general::font_status(
&zh,
&CjkInstall::Failed {
class,
message: "x".to_owned(),
},
);
assert!(value.contains(reason), "失败原因没进文案:{value}");
}
// 下载中写的是**真实**字节数(17 772 300 B → 16.9 MiB),不是假百分比。
let (value, _) = general::font_status(&zh, &CjkInstall::Downloading { bytes: 17_772_300 });
assert!(
value.contains("16.9 MB"),
"17 772 300 B ≈ 16.9 MiB:{value}"
);
assert!(!value.contains('%'), "没有真进度就不许写百分比:{value}");
}
/// 左列图标(`IconKind::Settings`)。分组图标是左列的语义标签,换掉就是换观感。
#[test]
fn the_general_group_keeps_its_icon() {
assert_eq!(general::GROUP.icon, IconKind::Settings);
}
/// **回归绊线(用户原话)**:`"English (System)"` 那条硬编码字面量不许回来。
///
/// 抓得住:①原来那条「这一组还没接真控件」在接上之后没更新(静默变假绿);
/// ②语言行**退回静态演示行**(哪怕换个说法,只要还写着「English」就是同一个 bug 的复盘)。
/// 抓不住:界面上真控件长什么样、三档选项名对不对(下一条与截图各管一段)。
///
/// 现状(M106):`body` 仍是 `None`(语言行走 `GENERAL_GROUP` 的按下标分发),但这一组**已经**
/// 有真控件了——所以这里断言的**不再是「还没接」**,而是「不许回到静态行」。
#[test]
fn the_language_row_is_not_a_static_string() {
for (label, value) in general::GROUP.rows {
assert_ne!(
*label, "Language",
"语言行必须走真控件(pages::Language),不许回到静态演示行"
);
assert!(
!value.contains("English"),
"静态行里不许再出现写死的语言名:{value}"
);
}
}
/// 语言行**三档**,且在**两套目录**里都有词(中文界面不该出现 `⟪missing⟫`)。
///
/// 抓得住:①少一档(用户就没法切回去了);②标签 key 只加进了中文表(英文界面上一档变成
/// `⟪missing⟫`);③回显那句的占位符名字与调用点对不上(会原样印出 `{language}`)。
/// 抓不住:译文翻得对不对(金表 + `chinese_catalog_is_actually_chinese` 管那一层)。
#[test]
fn the_language_row_offers_three_choices_in_both_languages() {
assert_eq!(
LangChoice::ALL.len(),
3,
"跟随系统 / 简体中文 / 英文,少一档就切不回来"
);
for lang in Lang::ALL {
let catalog = lang.catalog();
for choice in LangChoice::ALL {
assert!(
catalog.get(choice.label_key()).is_some(),
"{} 在 {} 目录里没词",
choice.label_key().name(),
lang.tag()
);
}
// 「界面当前使用:⟨语言名⟩」——中文系统下这句必须真的出现「简体中文」。
let effective = catalog.text(LangChoice::Fixed(*lang).label_key());
let line = catalog.render(Key::SettingsLanguageEffective, &[("language", effective)]);
assert!(line.contains(effective), "回显没带上语言名:{line}");
assert!(!line.contains('{'), "占位符没被替换:{line}");
}
// 中文系统 + 跟随系统 ⇒ 回显是「简体中文」(用户报的 bug 的直接判据)。
let zh = Lang::ZhCn.catalog();
assert_eq!(
zh.render(
Key::SettingsLanguageEffective,
&[(
"language",
zh.text(LangChoice::Fixed(Lang::ZhCn).label_key())
)]
),
"界面当前使用:简体中文"
);
}
/// **绊线**:这一组仍不走 `Group.body`。
///
/// 为什么不是「接了真控件」:语言行的入口是 `pages::Language`(`GENERAL_GROUP` 那条分发),
/// 而 `Group.body` 那个槽位只拿到 `States` 与**生效语言**(`Lang`,M179)——装不下「当前语言档位」。
/// 谁把语言行塞进 `body`,就得先给它换签名(那会改到别人那个组的文件),这条断言逼他先想一遍。
#[test]
fn the_general_group_does_not_use_the_appearance_body_slot() {
assert!(
general::GROUP.body.is_none(),
"General 的真控件走 pages::Language 入口(见 settings/mod.rs 的 GENERAL_GROUP),不是 body"
);
}
/// **M188:日志文件夹那一行**的文案必须与实现一致(用户投诉「找不到崩溃日志」的直接判据)。
///
/// 抓得住:①文案里的路径写错(写成 `data/logs`、`cache/game`);②滚动上限与
/// `core::launch::launcher_log::MAX_BYTES` 脱钩(文案说 1 MiB、实现改成 4 MiB);
/// ③旧目录被静默改成「已迁移」(那会让用户以为旧日志没了);
/// ④某条 key 只进了英文表(中文界面上是 `⟪missing⟫`)。
/// 抓不住:界面画出来长什么样(截图验收管那一层)。
#[test]
fn the_logs_row_names_the_real_paths_and_the_real_limit() {
use unlucky_core::launch::launcher_log;
use unlucky_core::platform::layout::{LauncherLayout, GAME_LOG_DIR, LOGS_DIR};
// 实现侧的真值:分区名与上限都只从 core 来。
assert_eq!(LOGS_DIR, "logs");
assert_eq!(GAME_LOG_DIR, "game");
assert_eq!(launcher_log::MAX_BYTES, 1024 * 1024, "文案说 1 MiB");
let layout = LauncherLayout::new(std::path::Path::new("/tmp/ull-home"));
assert_eq!(
layout.game_logs(),
std::path::Path::new("/tmp/ull-home/logs/game")
);
for lang in Lang::ALL {
let catalog = lang.catalog();
let hint = catalog.text(Key::SettingsLogsFolderHint);
for needle in [
"launcher.log",
"1 MiB",
&format!("{LOGS_DIR}/{GAME_LOG_DIR}/"),
"data/agent/sessions",
] {
assert!(
hint.contains(needle),
"{} 缺 `{needle}`:{hint}",
lang.tag()
);
}
assert!(!hint.contains('⟪'), "{} 目录里缺词:{hint}", lang.tag());
assert!(
!catalog.text(Key::SettingsLogsFolder).contains('⟪'),
"{} 缺「日志文件夹」:{}",
lang.tag(),
catalog.text(Key::SettingsLogsFolder)
);
assert!(!catalog.text(Key::SettingsLogsCopy).contains('⟪'));
assert!(!catalog.text(Key::SettingsLogsUnknown).contains('⟪'));
}
}