Files
Unlucky-Launcher/crates/app-iced/src/app/adopt/tests.rs
T
AskaEth fa3fab9086 fix(app-iced): 复审 r1 三条(在飞守卫补 Back/Target、预检按版本过期、shell 文档如实)
P2-1(行为):`Busy::Importing` 的早退只盖了 Close。「返回」会派 Scan、「换目标档」会派
零写盘 Preview,两条都把 `adopt_epoch` +1,于是在飞的 Confirm 交卷时 Done 被代次丢掉——
实例写进去了,界面什么都没说(正是 Close 守卫要防的形状)。两处补同款早退,且守卫写在
改本地状态**之前**:挡下来的换档不该把界面改成 B 档而盘上写的是 A 档。

P2-2(显示陈旧):`Form::fresh_plan` 原来只按目标档过期。先选 1.20.1 + 版本目录档,再改选
1.19.4,目标档没变,旧预检的 notes(说的是 `versions/1.20.1`)会继续当新版本的后果原文画。
现在连 `plan.version_id == selected` 一起判;没选中版本时也返回 None。同处那句
「版本一致性由 Self::candidate 那一路保证(选中即重扫)」与代码不符(Select 只改本地字段、
不发命令),改成如实说明过期判定只能在这里做。

P2-3(文档):`app/adopt/shell.rs` 的「纯搬移:函数体一行未改」不成立——同一提交里卡壳几何、
页脚去重、预览摘要都改过。改成逐条列出改了什么与为什么。

用例(两条都是先写红再修,红时 panic 信息就是错在哪):
- back_and_target_are_ignored_while_the_registration_is_in_flight(记账后端断言「不许派
  Scan / 不许派 Preview」);红:`登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)`;
- fresh_plan_expires_when_the_selected_version_changes;红:`换了版本之后旧预检必须过期
  (否则画的是 1.20.1 的后果原文)`。

顺带补图:`ULL_ADOPT_STATE` 加 `pick-selected` 一档(选中行底色 + 预检启用态),
出图 `DevDocs/artifacts/m223/adopt-pick-selected-light.png`(r1 复审说这格只有代码没截图)。
2026-11-10 19:37:45 +08:00

288 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.
//! `app::adopt` 的用态用例(M223):表单态 + 四段投影。
//!
//! 抓得住:
//! - `Form` 的生命周期(open 清上一轮 / close 全清 / settled 解除等待);
//! - `candidate()` 在选中项不在列表里时返回 `None`(界面自动退回「请选择」);
//! - 四段的文案真源(标题/后果原文/问题逐条)能从 `AdoptState` 投影出来。
//!
//! 抓不住:**像素**(那是 D30 的截图通道);也不抓 core 的判据(那是 `host/adopt/tests.rs`)。
use unlucky_i18n::{Key, Lang};
use unlucky_presenter::dto::{
AdoptCandidateView, AdoptDoneView, AdoptEvent, AdoptFailureView, AdoptIsolationView,
AdoptPlanView, AdoptProblemView, AdoptScanView, AdoptStage, AdoptState, AdoptTargetView,
};
use super::*;
fn candidate(version_id: &str, problems: Vec<AdoptProblemView>) -> AdoptCandidateView {
AdoptCandidateView {
version_id: version_id.to_string(),
jar_present: true,
inherits: Vec::new(),
registered: Vec::new(),
problems,
}
}
fn state_with_scan() -> AdoptState {
let mut state = AdoptState::default();
state.apply(AdoptEvent::Scan(Box::new(AdoptScanView {
versions_dir: std::path::PathBuf::from("/game/versions"),
candidates: vec![candidate("1.20.1", Vec::new())],
})));
state
}
/// 打开 ⇒ 关闭:上一轮的选中与名字都要清掉(否则下次打开画的是上一次的草稿)。
#[test]
fn open_and_close_reset_the_form() {
let mut form = Form::default();
form.open();
assert!(form.open);
assert_eq!(form.busy, Busy::Scanning);
assert_eq!(form.target, AdoptTargetView::InstanceDir);
form.selected = Some("1.20.1".to_string());
form.name = "生存".to_string();
form.close();
assert!(!form.open);
assert!(form.selected.is_none());
assert!(form.name.is_empty());
// 再开一次也不该带着上一轮的选中项
form.open();
assert!(form.selected.is_none());
}
/// `settled` 只解除等待,不动用户填的东西(回程不该清表单)。
#[test]
fn settling_keeps_what_the_user_typed() {
let mut form = Form::default();
form.open();
form.selected = Some("1.20.1".to_string());
form.name = "生存".to_string();
form.settled();
assert_eq!(form.busy, Busy::Idle);
assert_eq!(form.selected.as_deref(), Some("1.20.1"));
assert_eq!(form.name, "生存");
}
/// 选中项不在候选列表里 ⇒ `candidate()` 是 `None`(界面不能对着一个不存在的版本画「预检」)。
#[test]
fn candidate_is_none_when_the_selection_vanished() {
let state = state_with_scan();
let mut form = Form {
selected: Some("1.20.1".to_string()),
..Form::default()
};
assert!(form.candidate(&state).is_some());
form.selected = Some("9.9.9".to_string());
assert!(form.candidate(&state).is_none());
}
/// 四段的标题与后果原文都从目录来(不写死英文字面量、不重写 core 的后果句)。
#[test]
fn the_four_stages_project_from_the_event_state() {
let catalog = Lang::ZhCn.catalog();
let mut state = state_with_scan();
// Pick
assert_eq!(state.stage(), AdoptStage::Pick);
assert_eq!(
shell::title_text(state.stage(), catalog),
catalog.text(Key::AdoptTitle),
"标题走目录"
);
assert!(shell::subtitle(state.stage(), &state, catalog).contains("不复制"));
// Preview:`notes` 是 core `target_notes()` 的原文,界面一个字都不许改
let note =
"运行目录 = /game(共享档):mods/saves/config 会落在别人的 .minecraft 里".to_string();
state.apply(AdoptEvent::Planned(Box::new(AdoptPlanView {
version_id: "1.20.1".to_string(),
name: "1.20.1".to_string(),
target: AdoptTargetView::Shared,
isolation: AdoptIsolationView::Shared,
run_dir: std::path::PathBuf::from("/game"),
notes: vec![note.clone()],
})));
assert_eq!(state.stage(), AdoptStage::Preview);
assert_eq!(
state.plan.as_ref().map(|plan| plan.notes.first().cloned()),
Some(Some(note.clone())),
"后果原文必须原样留着"
);
// Failed:problems 逐条(不折叠)
state.apply(AdoptEvent::Failed(Box::new(AdoptFailureView {
code: "adopt_not_adoptable".to_string(),
message: "版本 1.19.4 不能登记".to_string(),
problems: vec![
AdoptProblemView {
code: "manifest_missing".to_string(),
message: "没有清单".to_string(),
},
AdoptProblemView {
code: "missing_parent".to_string(),
message: "缺父版本 1.20.1".to_string(),
},
],
})));
assert_eq!(state.stage(), AdoptStage::Failed);
assert_eq!(
state.failure.as_ref().map(|f| f.problems.len()),
Some(2),
"两条问题一条都不许吞"
);
assert_eq!(
shell::title_text(state.stage(), catalog),
catalog.text(Key::AdoptFailureTitle)
);
// Done
state.apply(AdoptEvent::Done(Box::new(AdoptDoneView {
instance_id: "id-1".to_string(),
name: "1.20.1".to_string(),
version_id: "1.20.1".to_string(),
isolation: AdoptIsolationView::Shared,
run_dir: std::path::PathBuf::from("/game"),
dir_created: false,
notes: vec![note],
})));
assert_eq!(state.stage(), AdoptStage::Done);
assert_eq!(
shell::title_text(state.stage(), catalog),
catalog.text(Key::AdoptDoneTitle)
);
}
/// 中英两套目录的向导文案都不是占位(`text()` 回落到 key 名就是这个用例要抓的假绿)。
#[test]
fn both_catalogs_have_the_wizard_wording() {
for lang in [Lang::En, Lang::ZhCn] {
let catalog = lang.catalog();
for key in [
Key::AdoptTitle,
Key::AdoptSubtitle,
Key::AdoptScanning,
Key::AdoptEmpty,
Key::AdoptSelectLabel,
Key::AdoptReady,
Key::AdoptTargetLabel,
Key::AdoptTargetInstanceDir,
Key::AdoptTargetShared,
Key::AdoptTargetVersionDir,
Key::AdoptNameLabel,
Key::AdoptPreview,
Key::AdoptConfirm,
Key::AdoptBack,
Key::AdoptClose,
Key::AdoptPlanTitle,
Key::AdoptPlanRunDir,
Key::AdoptPlanIsolation,
Key::AdoptFailureTitle,
Key::AdoptProblemCount,
Key::AdoptDoneTitle,
Key::AdoptDoneHint,
] {
let value = catalog.text(key);
assert!(!value.is_empty(), "{key:?} 在 {lang:?} 是空的");
assert_ne!(
value,
format!("{key:?}"),
"{key:?} 在 {lang:?} 没进目录(回落成了 key 名)"
);
}
}
// 共享档的短提示必须把「会写进别人的 .minecraft」说清楚(选之前就得知道)
let hint = Lang::En.catalog().text(Key::AdoptTargetSharedHint);
assert!(hint.contains(".minecraft"), "{hint}");
}
/// 「点选共享档 ⇒ 立刻看到后果原文」但**不**把用户甩到确认页:
/// 预检结果到了也只是 Pick 段里的提示,直到用户点过「预检」(`checked`) 才进 Preview。
///
/// 抓得住:把 `Form::stage` 写回直接取 `AdoptState::stage()`(点一次共享档就跳到确认页,
/// 实例名还没来得及改);或忘了 `fresh_plan` 的目标一致性判据(改档之后画上一档的后果原文)。
#[test]
fn a_foreign_target_shows_its_notes_without_jumping_to_confirm() {
let mut state = state_with_scan();
let note =
"运行目录 = /game(共享档):mods/saves/config 会落在别人的 .minecraft 里".to_string();
state.apply(AdoptEvent::Planned(Box::new(AdoptPlanView {
version_id: "1.20.1".to_string(),
name: "1.20.1".to_string(),
target: AdoptTargetView::Shared,
isolation: AdoptIsolationView::Shared,
run_dir: std::path::PathBuf::from("/game"),
notes: vec![note.clone()],
})));
let mut form = Form {
open: true,
selected: Some("1.20.1".to_string()),
target: AdoptTargetView::Shared,
..Form::default()
};
// 自动预检回来但用户没点「预检」:仍是 Pick 段,notes 当作提示可读
assert_eq!(form.stage(&state), AdoptStage::Pick);
assert_eq!(
form.fresh_plan(&state)
.map(|plan| plan.notes.first().cloned()),
Some(Some(note.clone())),
"后果原文必须能取到"
);
// 点过「预检」之后才进确认段
form.checked = true;
assert_eq!(form.stage(&state), AdoptStage::Preview);
// 换了目标档:上一档的预检作废(不再当后果原文画)
form.target = AdoptTargetView::VersionDir;
assert!(form.fresh_plan(&state).is_none(), "过期的预检不许画");
}
/// **P2-2**:`fresh_plan` 必须按**版本 id** 过期,不只是目标档。
///
/// 场景:先选 1.20.1 + 版本目录档(notes 说 `versions/1.20.1`),再改选 1.19.4——目标档没变,
/// 旧的预检结果若不按版本过滤就会继续当「这个版本的后果原文」画出来,那是一句假话。
///
/// 抓得住:`fresh_plan` 只看 `plan.target == self.target`(第一次修法就是这样漏的)。
/// 抓不住:core 真的会不会为 1.19.4 算出同一段 notes(那是 `target_notes()` 的活)。
#[test]
fn fresh_plan_expires_when_the_selected_version_changes() {
let mut state = state_with_scan();
state.apply(AdoptEvent::Planned(Box::new(AdoptPlanView {
version_id: "1.20.1".to_string(),
name: "1.20.1".to_string(),
target: AdoptTargetView::VersionDir,
isolation: AdoptIsolationView::Isolated,
run_dir: std::path::PathBuf::from("/game/versions/1.20.1"),
notes: vec!["运行目录 = 版本目录 /game/versions/1.20.1".to_string()],
})));
let mut form = Form {
open: true,
selected: Some("1.20.1".to_string()),
target: AdoptTargetView::VersionDir,
checked: true,
..Form::default()
};
assert!(
form.fresh_plan(&state).is_some(),
"对得上版本与目标档时应当可读"
);
// 改选另一个版本:目标档没变,但旧预检说的是别的版本的目录
form.selected = Some("1.19.4".to_string());
assert!(
form.fresh_plan(&state).is_none(),
"换了版本之后旧预检必须过期(否则画的是 1.20.1 的后果原文)"
);
// 没选版本时同样不该画(`None` = 没有任何版本的预检可言)
form.selected = None;
assert!(form.fresh_plan(&state).is_none());
}