五审:菜单文字/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+单行说明,**无需滚动**。
332 lines
11 KiB
Rust
332 lines
11 KiB
Rust
//! 安装选项的决策单测(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 的说明"
|
||
);
|
||
}
|
||
}
|