Files
Unlucky-Launcher/crates/app-iced/src/app/adopt/tests.rs
T
AskaEth 41caae3571 feat(app-iced): 实例页「导入已有版本」向导(M223)
页头「导入」按钮(以前是占位动作,点了只打日志)现在打开四段向导:

| 段 | 画什么 |
|---|---|
| Pick | 候选版本(可导入的才可点)+ 目标三档 chip + 实例名(缺省 InstanceDir) |
| Preview | 生效运行目录(回程给的)+ 隔离档 + target_notes() 原文逐条 |
| Failed | 稳定码 + problems 逐条(不折叠成一句「导入失败」) |
| Done | 实例名 + 运行目录 + 提示原文 |

- 向导是叠在页面之上的一层(遮罩 + 居中卡),不搅乱 list-detail 的版式判据;
- 表单态在 App(app/adopt.rs 的 Form),事件真相在 presenter 的 AdoptState;
- 在等回程时主按钮禁用(连点「确认」不会写两条实例);
- i18n 中英目录 + 金表补 27 条向导文案(实例域末尾追加)。
2026-11-10 19:37:42 +08:00

202 lines
6.9 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!(
title_text(&state, catalog),
catalog.text(Key::AdoptTitle),
"标题走目录"
);
assert!(subtitle(&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!(
title_text(&state, 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!(
title_text(&state, 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}");
}