Files
Unlucky-Launcher/crates/app-iced/src/pages/download/model/tests.rs
T
tower 6453c76750 fix(download): M94 r2 —— 安装终态出口 + 分类真机过滤(复审 P1-1 / P2-1)
P1-1 安装一旦收尾就再也装不了第二个版本(终态面板没有出口)
- 页面侧:`Download::from_snapshot` 把 **Done / Cancelled** 当结束(`InstallView::finished`),
  面板让回列表 —— 装完一个版本可以直接选下一个继续装(图:r2-done / r2-done-next)。
  不清 `Failed`:失败态带着用户要的东西(停在哪个文件 + 去日志页)。
- 面板加真出口:失败态「Open logs」(实心)+「Dismiss」(文本 → `Message::CancelInstall`,
  host 侧对没在跑的任务是安全 no-op),修掉「失败后连重试都不可达」。
- 决策抽成纯函数 `install::panel_state()`(Hint / Ready / Install),`panel()` 只是它的画法,
  于是「能不能再装」可以被单测钉死,不用开图形环境。
- 两条新测试**不走 MockBackend**(它从不发 `InstallProgress`,终态永远跑不到 ⇒ 假绿):
  直接构造 `AppState` + `apply(CoreEvent::InstallProgress(..))`。

P2-1 分类 chip 在真 host 上是空控件
- `from_snapshot(state, category, catalog)` 收分类并按 `Category::matches` **本地过滤**
  (真 host 的 `ListVersions(_)` 丢掉分类参数、回全量清单);
- 分类不再从列表反推(`Category::of` 只留给演示首帧),`app/view.rs::model` 收分类、
  `refresh_view` 把上一份视图模型的分类带回去 ⇒ 事件回来 chip 不会跳回「全部」;
- `update.rs` 的 `SetCategory` 写分类后**立刻重建**(chip 与列表同一个谓词),再派真指令。

p3(顺带)
- 失败态与占位态共存:列表空/加载中时若安装还在跑,面板照样出现在底部(r2-installing-empty);
- 修 `install.rs` 的坏 rustdoc 链接;`install.rs` / `model.rs` 的测试模块外置到子文件守住 400 行。

测试文件:`pages/download/{model,install}/tests.rs`(新),`pages/download/tests.rs` 里的
`from_snapshot` 调用补分类参数。

实证(回退法):删掉 `.filter(|view| !view.finished())` → 两条终态测试红并点名
「Done 之后不许再占着安装面板」;删掉 `.filter(|category.matches(..))` → 两条分类测试红
(「只剩正式版」left=3 right=1)。放回修复 → 全绿。

门禁:fmt --check 干净、clippy -D warnings 干净、app-iced 280 + 集成 6 + i18n 23 全绿(-j 4)。
2026-11-02 21:36:28 +08:00

63 lines
2.6 KiB
Rust

//! 视图模型的判据(从 `model.rs` 外置:那边加上本轮的分类过滤会顶到 400 行)。
use unlucky_presenter::dto::{AncientKind, AppEvent, CoreEvent, VersionKind, VersionSummary};
use unlucky_presenter::state::AppState;
use unlucky_i18n::Lang;
use super::{Category, Download};
fn version(id: &str, kind: VersionKind) -> VersionSummary {
VersionSummary {
id: id.to_string(),
kind,
released: "2026-09-20".to_string(),
installed: false,
}
}
fn state_with(versions: Vec<VersionSummary>) -> AppState {
let mut state = AppState::new();
state.apply(AppEvent::Core(CoreEvent::VersionListChanged(versions)));
state
}
/// 分类筛选是**本页的过滤**(复审 P2-1:真 host 的 `ListVersions(_)` 丢掉分类参数、回全量清单)。
///
/// 抓得住:`from_snapshot` 忘了按分类过滤——那正是「chip 点亮一下、列表照旧全量、事件回来 chip
/// 跳回『全部』」的根因;也抓得住「分类被列表反推覆盖」(`Category::of` 回到 `All`)。
/// 抓不住:真 host 是否按分类拉清单(`host/dispatch.rs:30` 丢参数,已报 finding)。
#[test]
fn the_category_filters_the_list_locally() {
let state = state_with(vec![
version("1.21.9", VersionKind::Release),
version("1.21.9-pre3", VersionKind::Snapshot),
version("rd-132211", VersionKind::Ancient(AncientKind::Classic)),
]);
let release = Download::from_snapshot(&state, Category::Release, Lang::En.catalog());
assert_eq!(
release.category,
Category::Release,
"谁选的就是谁:不从列表反推(反推会把混合列表打回 All)"
);
assert_eq!(release.rows.len(), 1, "只剩正式版");
assert_eq!(release.rows[0].id, "1.21.9");
let all = Download::from_snapshot(&state, Category::All, Lang::En.catalog());
assert_eq!(all.rows.len(), 3, "「全部」不过滤");
}
/// 分类把行全滤掉时是**空态**,不是加载态(两态文案与位置不同,别混)。
///
/// 抓得住:把「筛出来是空」当成「还没拉到」。
/// 抓不住:用户看到空态之后会怎么操作(那是 UI 层)。
#[test]
fn a_category_that_hides_everything_is_the_empty_state() {
let state = state_with(vec![version("1.21.9", VersionKind::Release)]);
let ancient = Download::from_snapshot(&state, Category::Ancient, Lang::En.catalog());
assert!(ancient.rows.is_empty());
assert!(!ancient.is_loading(), "快照里有版本 ⇒ 不是加载中");
assert!(ancient.is_empty(), "筛掉所有行 ⇒ 空态");
}