Files
Unlucky-Launcher/crates/app-iced/src/pages/download/tests.rs
T
AskaEth 89a51a63b1 feat(app-iced): M107 DLVRAIL —— 版本管理页三栏侧栏 + 跨类型搜索
用户令(信息架构):这一页从「下载」改称「版本管理」,版面从「一排 tab + 平铺列表」
改成**页面内三栏**:版本类型栏 + 主版本栏 + 列表/详情,并加搜索框。

- **三栏几何**(`pages/layout/rails.rs`,纯函数 + 数值表):类型栏 140/64、主版本栏 140/64、
  间距 24、列表列下限 424(行宽红线)。四档由**可用宽**裁决:Wide ≥888 / Narrow ≥676 /
  Compact ≥600 / Chips <600,三个门槛都是 `栏宽 + 间距 + 424` 算出来的,不是拍的
- **主版本分组规则**(`download/group.rs`):正式版/预发布按 `major.minor`(`1.21.9-pre3` → `1.21`)、
  周更按年桶(`24w14a` → `24w`)、远古版**没有主版本**(`rd-132211`/`a1.2.6` → 第二级只有「全部」);
  判据用真实版本样本钉住,不按「第一个点」切
- **跨类型搜索**(`search_bar` + 清空 + 命中数):查询词写进 `AppState.download_query`(页面无状态,
  只能这么活),`hits` 是**跨分类**读数;三条口径分开:「有命中」/「没有匹配」/「命中在别的类型里」
- 两级侧栏都用 `navigation::list_item` / `tabs` / `chip` / `buttons::icon`:Tab 可达、hover/pressed/
  选中态全部来自组件,页面不自己写状态层
- 右栏展开(M104 `content_width` 852)⇒ 可用宽 716 ⇒ Narrow 档,列表 464(任务书那句 492 漏扣
  96px 的页边距与栏间距,报告里给了完整数值表)
- 文案 11 条中英进 `crates/i18n`(新域宏接在装配链末尾 ⇒ `Key::ALL` 只追加,金表只追加)
- 护栏:两层侧栏都存在 + 列表按第二级选中态过滤(变异可证)、三档门槛、424 下限扫描、
  版本行单行宽账、搜索条真的装着
- `install.rs` 只动了「已装」分支(加一句「已安装」解释禁用按钮),没碰 M105 的选项区

取证:`DevDocs/artifacts/m107/`(10 张:三档 × 浅深 + 搜索命中 + 空 + 异地 + 远古版)
2026-11-05 01:59:13 +08:00

318 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.
//! 版本管理页的判据:**快照 → 模型 → 控件树**三段各自的断言。
//!
//! 每条都写清「抓得住什么 / 抓不住什么」——这里只测纯转换与树能构建,**像素**由
//! `DevDocs/artifacts/m107/` 的十张图与报告的量测表负责(同 `pages/tests.rs` 的口径)。
use unlucky_design::color::SchemeFlavor;
use unlucky_design::typography::Platform;
use unlucky_i18n::Lang;
use unlucky_presenter::dto::{
AncientKind, AppEvent, CoreEvent, InstallProgressView, InstallStage, VersionCategory,
VersionKind, VersionSummary,
};
use unlucky_presenter::state::AppState;
use crate::theme::{Families, Ui};
use super::super::layout::{frame_metrics, measure_cap_px, PaneKit};
use super::model::{Category, Download};
use super::options::InstallDraft;
mod i18n;
const FAMILIES: Families = Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
};
fn ui() -> Ui {
Ui::with_platform(SchemeFlavor::Light, Platform::Windows, FAMILIES)
}
fn version(id: &str, kind: VersionKind, installed: bool) -> VersionSummary {
VersionSummary {
id: id.to_string(),
kind,
released: "2026-09-20".to_string(),
installed,
}
}
fn state_with(versions: Vec<VersionSummary>) -> AppState {
let mut state = AppState::new();
state.apply(AppEvent::Core(CoreEvent::VersionListChanged(versions)));
state
}
/// 一份覆盖四种版本形态的清单(`1.21.x` / `24w14a` / `rd-132211` / `a1.2.6`)。
fn mixed() -> Vec<VersionSummary> {
vec![
version("1.21.9", VersionKind::Release, false),
version("1.21.8", VersionKind::Release, true),
version("1.20.4", VersionKind::Release, true),
version("24w14a", VersionKind::Snapshot, false),
version(
"rd-132211",
VersionKind::Ancient(AncientKind::Classic),
false,
),
]
}
// --------------------------------------------------------------- 版面
/// 页面仍是单列页(页内三栏是**页内**的事,不改 `PaneKit`):任何宽度都不出现「页面级第二列」。
///
/// 抓得住:有人把 `Screen::Download` 的 `kit()` 改成 `ListDetail`(那会让页头与页边距跟着变)。
/// 抓不住:页内三栏的宽度(那由 `layout/rails/tests.rs` 的数值表钉住)。
#[test]
fn the_page_kit_is_still_single() {
for width in [700.0, 900.0, 1024.0, 1180.0, 2400.0] {
let metrics = frame_metrics(PaneKit::Single, width);
assert!(metrics.secondary.is_none(), "{width} 出现了页面级第二列");
}
}
/// 宽窗下多出来的宽度归**页面块**,不归散文列(M102 订正;旧口径是「留白」)。
///
/// 抓得住:谁把整页块又压回 424(视觉复审 ① 的原样:块宽 = `column`,1180 下左右各留 ~300px);
/// 抓不住:真实像素(报告里的量测表负责)。
#[test]
fn extra_width_goes_to_the_page_block_not_to_the_prose_column() {
let metrics = frame_metrics(PaneKit::Single, 1180.0);
assert_eq!(metrics.column, measure_cap_px(), "散文列仍是行宽上限 424");
assert_eq!(
super::super::frame::body_width(metrics),
metrics.available,
"页面块必须吃满可用宽度(1044),不是被居中压成 424"
);
assert!(
metrics.available - metrics.column >= 400.0,
"两个量要分得开:块宽 1044 比散文列 424 宽出 620"
);
}
// --------------------------------------------------------------- 两层侧栏(见 `tests/rails.rs`)
mod rails;
// --------------------------------------------------------------- 文案(i18n)
// --------------------------------------------------------------- 快照 → 模型
/// 真快照进得来:行数、分类、选中项、已装标记、主版本桶全部来自 `AppState`。
#[test]
fn the_snapshot_becomes_the_page_model() {
let mut state = state_with(vec![
version("1.21.9", VersionKind::Release, false),
version("1.21.8", VersionKind::Release, true),
]);
state.apply(AppEvent::Core(CoreEvent::InstallProgress(Some(
InstallProgressView {
version: "1.21.9".to_string(),
loader: "Vanilla".to_string(),
stage: InstallStage::Downloading,
files_done: 128,
files_total: 512,
speed_bps: 1_258_291,
eta_secs: 75,
..InstallProgressView::new()
},
))));
let model = Download::from_snapshot(
&state,
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert_eq!(model.rows.len(), 2);
assert_eq!(model.rows[0].id, "1.21.9");
assert!(model.rows[1].installed, "快照说装了就是装了");
assert_eq!(model.category, Category::All);
assert_eq!(
model.selected.as_deref(),
Some("1.21.9"),
"正在安装的版本 = 选中行"
);
assert!(model.loaded, "有版本 = 已加载");
assert_eq!(model.buckets.len(), 1, "两个版本同一个主版本桶");
assert_eq!(model.buckets[0].key.as_deref(), Some("1.21"));
assert_eq!(model.hits, 2, "没有搜索词时命中数 = 行数");
let install = model.install.expect("安装进度要转进来");
assert!(install.running());
assert_eq!(install.detail, "128/512 files");
}
/// 快照里的两个过滤条件(`installed_only` / `download_query`)原样照用。
#[test]
fn presenter_filters_still_apply() {
let mut state = state_with(vec![
version("1.21.9", VersionKind::Release, false),
version("1.21.8", VersionKind::Release, true),
version("1.20.4", VersionKind::Release, true),
]);
let installed_only = {
state.installed_only = true;
Download::from_snapshot(
&state,
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
)
};
assert_eq!(installed_only.rows.len(), 2, "仅已装");
let queried = {
state.installed_only = false;
state.download_query = " 1.21 ".to_string();
Download::from_snapshot(
&state,
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
)
};
assert_eq!(queried.rows.len(), 2, "版本号片段匹配两个");
assert_eq!(queried.query, "1.21", "查询词去掉首尾空格后进模型");
assert_eq!(queried.hits, 2);
}
/// 空快照 = 加载态;过滤掉所有行 = 空态。
#[test]
fn loading_and_empty_are_different_states() {
let loading = Download::from_snapshot(
&AppState::new(),
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert!(loading.is_loading() && !loading.is_empty());
let mut state = state_with(vec![version("1.21.9", VersionKind::Release, false)]);
state.download_query = "no-such-version".to_string();
let empty = Download::from_snapshot(
&state,
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert!(!empty.is_loading(), "列表拉到了,不是加载中");
assert!(empty.is_empty(), "过滤后没有行 = 空态");
assert!(empty.no_match(), "跨类型也没有 ⇒ 没有匹配");
assert!(!empty.only_elsewhere());
}
/// **搜索跨类型**:命中数与当前分类无关,行数相关。
///
/// 抓得住:有人把 `hits` 也算成「当前分类的命中」(那会让「正式版里搜 24w」显示 0 个匹配,
/// 用户以为没有这个版本)。
/// 抓不住:大小写之外的搜索口径(模糊匹配 / 分词属 presenter)。
#[test]
fn search_hits_are_cross_category() {
let mut state = state_with(mixed());
state.download_query = "1.20".to_string();
let all = Download::from_snapshot(
&state,
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert_eq!(all.rows.len(), 1);
assert_eq!(all.hits, 1);
// 同一个词,站在「预览版」分类里:行数 0,命中数仍是 1
let snapshot = Download::from_snapshot(
&state,
Category::Snapshot,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert_eq!(snapshot.rows.len(), 0);
assert_eq!(snapshot.hits, 1, "命中数是跨类型读数");
assert!(snapshot.only_elsewhere(), "有命中但不在当前分类");
assert!(!snapshot.no_match(), "不是「没有匹配」");
}
/// 「搜不到」与「在当前分类搜不到」是两种画面(文案与出口都不同)。
#[test]
fn the_two_search_misses_are_distinguishable() {
let mut state = state_with(mixed());
state.download_query = "zzz".to_string();
let nothing = Download::from_snapshot(
&state,
Category::All,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert!(nothing.no_match() && !nothing.only_elsewhere());
state.download_query = "rd-".to_string();
let elsewhere = Download::from_snapshot(
&state,
Category::Release,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert!(elsewhere.only_elsewhere() && !elsewhere.no_match());
let home = Download::from_snapshot(
&state,
Category::Ancient,
&InstallDraft::default(),
Lang::En.catalog(),
);
assert_eq!(home.rows.len(), 1, "切到远古版就看见了");
assert!(!home.no_match() && !home.only_elsewhere());
}
/// 分类反推:混合 = 全部,远古 = 远古(不看子类)。
#[test]
fn the_category_is_derived_from_the_list() {
let mixed = vec![
version("1.21.9", VersionKind::Release, false),
version("1.21.9-pre3", VersionKind::Snapshot, false),
];
assert_eq!(Category::of(&mixed), Category::All);
assert_eq!(
Category::of(&[version("x", VersionKind::Snapshot, false)]),
Category::Snapshot
);
assert_eq!(
Category::of(&[version(
"rd-132211",
VersionKind::Ancient(AncientKind::OldBeta),
false
)]),
Category::Ancient
);
assert_eq!(Category::of(&[]), Category::All);
}
/// 分类 chip 派的是**真指令**。
#[test]
fn category_commands_are_real() {
assert_eq!(Category::All.command(), None, "全部 = 画现有快照,不派指令");
assert_eq!(Category::Release.command(), Some(VersionCategory::Release));
assert_eq!(
Category::Snapshot.command(),
Some(VersionCategory::Snapshot)
);
assert_eq!(
Category::Ancient.command(),
Some(VersionCategory::Ancient(AncientKind::Classic))
);
assert!(Category::Ancient.matches(VersionKind::Ancient(AncientKind::Unlisted)));
assert!(!Category::Release.matches(VersionKind::Snapshot));
}
// --------------------------------------------------------------- 控件树(见 `tests/view.rs`)
mod view;