Files
Unlucky-Launcher/crates/app-iced/src/pages/download/options/tests.rs
T
AskaEth cb2cffb3ca fix(app-iced,i18n): 分高改「面板先保预算」+ 说明压成单行(D30 五审一次性重分配)
五审:菜单文字/720 列表/44-97 标定/04 都过,但滚动区被压到 82px(宽)/24px(窄)——宽档切
「安装选项」字身、六枚 chip 0 像素。塔裁决停止逐轮调顺序,一次性重分配:

1. **分高从「对半分」改成「面板先保预算」**:`PANEL_BUDGET = 300`(滚动区 128 + 钉死区 136),
   `whole_rows = floor((column − LIST_CHROME − PANEL_BUDGET)/44) × 44`。
   1180×760:580−97−300 = 183 ⇒ **176px = 4 整行**(原 5 行),面板 307px ⇒ 滚动区 ≈150px;
   1180×900:323 ⇒ **308px = 7 整行**(未变)。720 档走 `None`(Fill),未动。
2. **说明压成单行**(`DownloadOptionsLiteLoaderVanillaOnly`):两行 28px 是滚动区最大的可压项。
   en `Vanilla parent only — Forge goes through the CLI` / zh `只叠原版;叠 Forge 请走命令行`;
   完整命令行说法留在 `ull help install`。单测断言随之改成「含该句 + 只有一行」并写明原因。

文档按六审要求改口:滚走的是说明/错误行(非控件);说明变单行后 760 档滚动区已能装下
head+标签+chips+单行说明,**无需滚动**。
2026-11-10 19:34:14 +08:00

332 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.
//! 安装选项的决策单测(M105):兼容性真值表 + 确认键门 + 默认加载器版本。
//!
//! 全部是**纯函数**(`OptionsView::of` 吃 `AppState` 与 `VersionRow`,不碰 iced),
//! 所以不开图形环境也能钉住「什么组合能装、什么组合按下不去」。
//!
//! **为什么用 `AppState` 而不是 mock 后端**:加载器版本列表是快照字段
//! (`state.loader_versions`),灌一条 `CoreEvent::LoaderVersions` 就能造出三种状态
//! (没查过 / 查到了 / 取不到),比 mock 更直接,也不依赖 `MockBackend` 的脚本。
use unlucky_i18n::Lang;
use unlucky_presenter::dto::{
AncientKind, AppEvent, CoreEvent, LoaderKind, VersionKind, VersionSummary,
};
use unlucky_presenter::state::AppState;
use super::model::{release_rank, Incompat, InstallDraft, Loader, LoaderVersionsView, OptionsView};
use crate::pages::download::model::VersionRow;
pub(super) fn row(id: &str, kind: VersionKind) -> VersionRow {
VersionRow::of(
&VersionSummary {
id: id.to_string(),
kind,
released: "2026-09-20".to_string(),
installed: false,
},
Lang::En.catalog(),
)
}
pub(super) fn state_with(kind: LoaderKind, mc: &str, items: Result<Vec<&str>, &str>) -> AppState {
let mut state = AppState::new();
state.apply(AppEvent::Core(CoreEvent::LoaderVersions {
kind,
mc_version: mc.to_string(),
result: items
.map(|list| {
list.into_iter()
.map(str::to_string)
.collect::<Vec<String>>()
})
.map_err(str::to_string),
}));
state
}
pub(super) fn draft(loader: Loader) -> InstallDraft {
InstallDraft {
selected: Some("1.20.1".to_string()),
loader,
loader_version: String::new(),
instance_name: String::new(),
}
}
/// 版本号解析:只有纯数字三段(或缺末段)才算正式版。
#[test]
fn release_rank_only_accepts_plain_release_ids() {
assert_eq!(release_rank("1.20.1"), Some((1, 20, 1)));
assert_eq!(release_rank("1.20"), Some((1, 20, 0)), "缺末段按 0 算");
assert_eq!(release_rank("24w14a"), None, "快照不是正式版号");
assert_eq!(release_rank("rd-132211"), None);
assert_eq!(
release_rank("1.20.1-pre3"),
None,
"预发布不许当正式版放过去"
);
assert_eq!(release_rank("1.20.1.2"), None, "多一段不认");
}
/// 兼容性真值表(任务书点名的「老版本 × NeoForge」在这条里)。
#[test]
fn loader_support_matrix_is_explicit() {
// 原版永远可以
for id in ["1.21.9", "1.12.2", "rd-132211"] {
let kind = if id.starts_with("rd") {
VersionKind::Ancient(AncientKind::Classic)
} else {
VersionKind::Release
};
assert_eq!(Loader::Vanilla.supports(id, kind), Ok(()));
}
// NeoForge 要 1.20.1+
assert_eq!(
Loader::NeoForge.supports("1.20.1", VersionKind::Release),
Ok(())
);
assert_eq!(
Loader::NeoForge.supports("1.12.2", VersionKind::Release),
Err(Incompat::NeedsAtLeast("1.20.1")),
"老版本 × NeoForge 必须在选中那一刻就说清,而不是点安装才失败"
);
// Fabric/Quilt 要 1.14+
assert_eq!(
Loader::Fabric.supports("1.20.1", VersionKind::Release),
Ok(())
);
assert_eq!(
Loader::Fabric.supports("1.12.2", VersionKind::Release),
Err(Incompat::NeedsAtLeast("1.14"))
);
// Forge 任何正式版都行,但预览版/远古版没有 maven 条目
assert_eq!(
Loader::Forge.supports("1.12.2", VersionKind::Release),
Ok(())
);
assert_eq!(
Loader::Forge.supports("24w14a", VersionKind::Snapshot),
Err(Incompat::Unsupported)
);
// LiteLoader 的官方版本表只有 1.5.2 ~ 1.12.2(16 个原版版本),两头都按「没有构建」拒。
for id in ["1.5.2", "1.7.10", "1.12.2"] {
assert_eq!(
Loader::LiteLoader.supports(id, VersionKind::Release),
Ok(()),
"{id} 官方的确有 LiteLoader"
);
}
for id in ["1.4.7", "1.13", "1.20.1"] {
assert_eq!(
Loader::LiteLoader.supports(id, VersionKind::Release),
Err(Incompat::Unsupported),
"{id} 上游没有 LiteLoader 构建"
);
}
assert_eq!(
Loader::LiteLoader.supports("24w14a", VersionKind::Snapshot),
Err(Incompat::Unsupported)
);
// 远古版:五家全都不支持
let ancient = ("a1.2.6", VersionKind::Ancient(AncientKind::OldAlpha));
for loader in [
Loader::Fabric,
Loader::Quilt,
Loader::Forge,
Loader::NeoForge,
Loader::LiteLoader,
] {
assert_eq!(
loader.supports(ancient.0, ancient.1),
Err(Incompat::Unsupported)
);
}
}
/// 不兼容的组合:给具名提示,并且**确认键按下不去**。
#[test]
fn incompatible_combination_disables_confirm_with_a_named_reason() {
let state = AppState::new();
let options = OptionsView::of(
&row("1.12.2", VersionKind::Release),
&state,
&draft(Loader::NeoForge),
Lang::En.catalog(),
);
assert!(!options.can_confirm, "不兼容就不许按下确认");
let problem = options.problem.expect("要给出具名原因");
assert!(problem.contains("NeoForge"), "{problem}");
assert!(problem.contains("1.20.1"), "{problem}");
}
/// 默认加载器版本 = 列表里最新的一条(core 的默认口径);用户选过就用用户的。
#[test]
fn loader_version_defaults_to_the_newest_and_honours_the_choice() {
let state = state_with(LoaderKind::Fabric, "1.20.1", Ok(vec!["0.16.5", "0.16.4"]));
let options = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&state,
&draft(Loader::Fabric),
Lang::En.catalog(),
);
assert!(options.can_confirm);
assert_eq!(options.loader_version, "0.16.5", "没选过就用最新");
assert_eq!(options.loader_version_arg(), Some("0.16.5"));
let mut chosen = draft(Loader::Fabric);
chosen.loader_version = "0.16.4".to_string();
let options = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&state,
&chosen,
Lang::En.catalog(),
);
assert_eq!(options.loader_version, "0.16.4", "用户选的就是用户选的");
}
/// 三条「列表还没到位」的状态都不许按下确认,且各自说清是哪种。
#[test]
fn versions_pending_or_failed_blocks_confirm() {
let bare = AppState::new();
let loading = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&bare,
&draft(Loader::Fabric),
Lang::En.catalog(),
);
assert_eq!(loading.versions, LoaderVersionsView::Loading);
assert!(!loading.can_confirm, "还没查过就不许装");
let failed = state_with(LoaderKind::Fabric, "1.20.1", Err("network down"));
let options = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&failed,
&draft(Loader::Fabric),
Lang::En.catalog(),
);
assert!(!options.can_confirm);
assert!(
options
.problem
.as_deref()
.is_some_and(|p| p.contains("network down")),
"取不到要给具名原因(不是空下拉)"
);
let empty = state_with(LoaderKind::Fabric, "1.20.1", Ok(vec![]));
let options = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&empty,
&draft(Loader::Fabric),
Lang::En.catalog(),
);
assert!(!options.can_confirm, "官方没有版本就不许装");
assert!(options.problem.is_some());
}
/// 缓存键带原版版本:A 版本的列表不会挂到 B 版本下面。
#[test]
fn cached_versions_are_keyed_by_minecraft_version() {
let state = state_with(LoaderKind::Fabric, "1.20.1", Ok(vec!["0.16.5"]));
let other = OptionsView::of(
&row("1.21.9", VersionKind::Release),
&state,
&draft(Loader::Fabric),
Lang::En.catalog(),
);
assert_eq!(
other.versions,
LoaderVersionsView::Loading,
"换原版版本就得重新查,不许复用上一版的结果"
);
}
/// 原版:不查列表、不需要加载器版本,确认键直接可用。
#[test]
fn vanilla_needs_no_loader_version_at_all() {
let state = AppState::new();
let options = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&state,
&draft(Loader::Vanilla),
Lang::En.catalog(),
);
assert!(options.can_confirm);
assert_eq!(options.versions, LoaderVersionsView::Ready(Vec::new()));
assert_eq!(options.loader_version, "");
assert_eq!(options.loader_version_arg(), None);
}
/// 已装的版本:确认键给「已安装」禁用态(列表行上也有勾,按钮再点一次不会更清楚)。
#[test]
fn installed_versions_cannot_be_confirmed_again() {
let state = AppState::new();
let mut installed = row("1.20.1", VersionKind::Release);
installed.installed = true;
let options = OptionsView::of(
&installed,
&state,
&draft(Loader::Vanilla),
Lang::En.catalog(),
);
assert!(options.installed);
assert!(!options.can_confirm);
}
/// 中性说明(M222):只有 LiteLoader 带「这里只叠原版」那条,且它**不影响**确认键。
///
/// 抓得住:说明被塞进 `problem`(会让用户以为不能装、确认键按不下去);或者说明跑到别家
/// 加载器下面(Forge/Fabric 没有这条边界)。
#[test]
fn liteloader_carries_a_neutral_note_without_blocking_confirm() {
let state = state_with(
LoaderKind::LiteLoader,
"1.12.2",
Ok(vec!["1.12.2-SNAPSHOT"]),
);
let options = OptionsView::of(
&row("1.12.2", VersionKind::Release),
&state,
&draft(Loader::LiteLoader),
Lang::En.catalog(),
);
let note = options.note.as_deref().expect("LiteLoader 应带中性说明");
// 文案在 D30 五审后压成**单行**(两行 28px 是滚动区最大的可压项),断言随之改为
// 「说清只支持原版父链、且确实只有一行」;完整命令行说法在 `ull help install` 里。
assert!(note.contains("Vanilla parent only"), "{note}");
assert_eq!(note.lines().count(), 1, "必须是一行:{note}");
assert!(options.problem.is_none(), "中性说明不能变成错误");
assert!(options.can_confirm, "说明不该禁用确认键");
// 中文目录同样给中文说明,且仍然不禁用确认键。
let zh = state_with(
LoaderKind::LiteLoader,
"1.12.2",
Ok(vec!["1.12.2-SNAPSHOT"]),
);
let options = OptionsView::of(
&row("1.12.2", VersionKind::Release),
&zh,
&draft(Loader::LiteLoader),
Lang::ZhCn.catalog(),
);
let note = options.note.as_deref().expect("中文也要有说明");
assert!(note.contains("只叠原版"), "{note}");
assert!(options.can_confirm);
// 别家加载器不带这条说明。
for loader in [Loader::Vanilla, Loader::Fabric, Loader::Forge] {
let state = state_with(loader.kind(), "1.20.1", Ok(Vec::new()));
let options = OptionsView::of(
&row("1.20.1", VersionKind::Release),
&state,
&draft(loader),
Lang::En.catalog(),
);
assert!(
options.note.is_none(),
"{loader:?} 不该带 LiteLoader 的说明"
);
}
}