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 张:三档 × 浅深 + 搜索命中 + 空 + 异地 + 远古版)
This commit is contained in:
1 parent
72b0c9d465
commit
89a51a63b1
24 files changed
+2000
-340
No files matched your search
@@ -1,7 +1,7 @@
|
||||
//! 下载页的判据:**快照 → 模型 → 控件树**三段各自的断言。
|
||||
//! 版本管理页的判据:**快照 → 模型 → 控件树**三段各自的断言。
|
||||
//!
|
||||
//! 每条都写清「抓得住什么 / 抓不住什么」——这里只测纯转换与树能构建,**像素**由报告里的
|
||||
//! 四张图与数值表负责(同 `pages/tests.rs` 的口径)。
|
||||
//! 每条都写清「抓得住什么 / 抓不住什么」——这里只测纯转换与树能构建,**像素**由
|
||||
//! `DevDocs/artifacts/m107/` 的十张图与报告的量测表负责(同 `pages/tests.rs` 的口径)。
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
@@ -14,8 +14,7 @@ use unlucky_presenter::state::AppState;
|
||||
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
use super::super::layout::{content_column_width, frame_metrics, measure_cap_px, PaneKit};
|
||||
use super::super::Message;
|
||||
use super::super::layout::{frame_metrics, measure_cap_px, PaneKit};
|
||||
use super::model::{Category, Download};
|
||||
use super::options::InstallDraft;
|
||||
|
||||
@@ -46,19 +45,32 @@ fn state_with(versions: Vec<VersionSummary>) -> AppState {
|
||||
state
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 版面(M84 的三条,M94 一条没动)
|
||||
/// 一份覆盖四种版本形态的清单(`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`):任何宽度都不出现「页面级第二列」。
|
||||
///
|
||||
/// 抓得住:`layout` 把下载页切到两列、或把正文列放过 424。
|
||||
/// 抓不住:页面内部又套了一层更宽的容器(那要看真像素,见报告的量测表)。
|
||||
/// 抓得住:有人把 `Screen::Download` 的 `kit()` 改成 `ListDetail`(那会让页头与页边距跟着变)。
|
||||
/// 抓不住:页内三栏的宽度(那由 `layout/rails/tests.rs` 的数值表钉住)。
|
||||
#[test]
|
||||
fn download_is_single_column_at_every_width() {
|
||||
for width in [600.0, 840.0, 900.0, 1180.0, 2400.0] {
|
||||
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} 宽下载页出现了第二列");
|
||||
assert!(metrics.column <= measure_cap_px() + f32::EPSILON);
|
||||
assert_eq!(metrics.column, content_column_width(width));
|
||||
assert!(metrics.secondary.is_none(), "{width} 出现了页面级第二列");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +93,15 @@ fn extra_width_goes_to_the_page_block_not_to_the_prose_column() {
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 两层侧栏(见 `tests/rails.rs`)
|
||||
|
||||
mod rails;
|
||||
|
||||
// --------------------------------------------------------------- 文案(i18n)
|
||||
|
||||
// --------------------------------------------------------------- 快照 → 模型
|
||||
|
||||
/// 真快照进得来:行数、分类、选中项、已装标记全部来自 `AppState`。
|
||||
/// 抓不住:快照字段在 `app/view.rs` 被漏接(那由 `app/update.rs` 的接线测试兜)。
|
||||
/// 真快照进得来:行数、分类、选中项、已装标记、主版本桶全部来自 `AppState`。
|
||||
#[test]
|
||||
fn the_snapshot_becomes_the_page_model() {
|
||||
let mut state = state_with(vec![
|
||||
@@ -114,32 +131,23 @@ fn the_snapshot_becomes_the_page_model() {
|
||||
assert_eq!(model.rows.len(), 2);
|
||||
assert_eq!(model.rows[0].id, "1.21.9");
|
||||
assert!(model.rows[1].installed, "快照说装了就是装了");
|
||||
assert!(!model.rows[0].installed);
|
||||
assert_eq!(
|
||||
model.category,
|
||||
Category::All,
|
||||
"分类是**调用方给的过滤条件**,不再从列表反推(反推正是 chip 跳回「全部」的来源)"
|
||||
);
|
||||
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(), "Downloading 还没收尾");
|
||||
assert!(!install.failed());
|
||||
assert!(install.running());
|
||||
assert_eq!(install.detail, "128/512 files");
|
||||
assert_eq!(
|
||||
install.rate.as_deref(),
|
||||
Some("1.2 MB/s · 1m15s left"),
|
||||
"速率与剩余时间走 key,不搬 core 的中文串"
|
||||
);
|
||||
}
|
||||
|
||||
/// 快照里的两个过滤条件(`installed_only` / `download_query`)原样照用。
|
||||
/// 抓不住:过滤词大小写之外的语言学规则(那属于 presenter 的搜索口径)。
|
||||
#[test]
|
||||
fn presenter_filters_still_apply() {
|
||||
let mut state = state_with(vec![
|
||||
@@ -170,11 +178,11 @@ fn presenter_filters_still_apply() {
|
||||
)
|
||||
};
|
||||
assert_eq!(queried.rows.len(), 2, "版本号片段匹配两个");
|
||||
assert!(queried.rows.iter().all(|row| row.id.starts_with("1.21")));
|
||||
assert_eq!(queried.query, "1.21", "查询词去掉首尾空格后进模型");
|
||||
assert_eq!(queried.hits, 2);
|
||||
}
|
||||
|
||||
/// 空快照 = 加载态;过滤掉所有行 = 空态(抓「把还没拉到画成没有版本」,或反过来)。
|
||||
/// 抓不住:真机上卡在加载态多久(没有超时信号,见模块文档的缺口表)。
|
||||
/// 空快照 = 加载态;过滤掉所有行 = 空态。
|
||||
#[test]
|
||||
fn loading_and_empty_are_different_states() {
|
||||
let loading = Download::from_snapshot(
|
||||
@@ -195,6 +203,73 @@ fn loading_and_empty_are_different_states() {
|
||||
);
|
||||
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());
|
||||
}
|
||||
|
||||
/// 分类反推:混合 = 全部,远古 = 远古(不看子类)。
|
||||
@@ -220,10 +295,7 @@ fn the_category_is_derived_from_the_list() {
|
||||
assert_eq!(Category::of(&[]), Category::All);
|
||||
}
|
||||
|
||||
/// 分类 chip 派的是**真指令**:`VersionCategory` 直接喂给 `AppCommand::ListVersions`。
|
||||
///
|
||||
/// 抓得住:chip 点到不存在的指令、或把 `All` 也派出去(那会白拉一次列表)。
|
||||
/// 抓不住:后端是否真按分类回列表(presenter/host 的测试负责)。
|
||||
/// 分类 chip 派的是**真指令**。
|
||||
#[test]
|
||||
fn category_commands_are_real() {
|
||||
assert_eq!(Category::All.command(), None, "全部 = 画现有快照,不派指令");
|
||||
@@ -234,112 +306,12 @@ fn category_commands_are_real() {
|
||||
);
|
||||
assert_eq!(
|
||||
Category::Ancient.command(),
|
||||
Some(VersionCategory::Ancient(AncientKind::Classic)),
|
||||
"远古版先拉 classic(四子类同屏要 presenter 支持,已登记 finding)"
|
||||
Some(VersionCategory::Ancient(AncientKind::Classic))
|
||||
);
|
||||
assert!(Category::Ancient.matches(VersionKind::Ancient(AncientKind::Unlisted)));
|
||||
assert!(!Category::Release.matches(VersionKind::Snapshot));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 控件树
|
||||
// --------------------------------------------------------------- 控件树(见 `tests/view.rs`)
|
||||
|
||||
/// 五个状态都能构建出控件树(分支走到、不 panic、消息类型对得上)。
|
||||
///
|
||||
/// 抓得住:某个状态分支漏了返回值、或消息类型接错(编译期就红);空态/加载态忘了给内容。
|
||||
/// 抓不住:**画出来长什么样**——像素由报告里的四张图负责,这条只保证「有树」。
|
||||
#[test]
|
||||
fn every_state_builds_a_tree() {
|
||||
let ui = ui();
|
||||
let metrics = frame_metrics(PaneKit::Single, 1180.0);
|
||||
let catalog = Lang::En.catalog();
|
||||
|
||||
let mut loading = Download::loading();
|
||||
loading.selected = None;
|
||||
|
||||
let empty = Download {
|
||||
loaded: true,
|
||||
..Download::loading()
|
||||
};
|
||||
|
||||
let ready = Download {
|
||||
options: None,
|
||||
rows: vec![super::model::VersionRow::of(
|
||||
&version("1.21.9", VersionKind::Release, false),
|
||||
catalog,
|
||||
)],
|
||||
category: Category::Release,
|
||||
selected: Some("1.21.9".to_string()),
|
||||
install: None,
|
||||
loaded: true,
|
||||
};
|
||||
|
||||
let installing = Download {
|
||||
install: Some(super::model::InstallView::of(
|
||||
&InstallProgressView {
|
||||
version: "1.21.9".to_string(),
|
||||
stage: InstallStage::Downloading,
|
||||
files_done: 128,
|
||||
files_total: 512,
|
||||
..InstallProgressView::new()
|
||||
},
|
||||
catalog,
|
||||
)),
|
||||
..ready.clone()
|
||||
};
|
||||
|
||||
let failed = Download {
|
||||
install: Some(super::model::InstallView::of(
|
||||
&InstallProgressView {
|
||||
version: "1.21.9".to_string(),
|
||||
stage: InstallStage::Failed,
|
||||
..InstallProgressView::new()
|
||||
},
|
||||
catalog,
|
||||
)),
|
||||
..ready.clone()
|
||||
};
|
||||
|
||||
for model in [loading, empty, ready, installing, failed] {
|
||||
let _: iced::Element<'_, Message> = super::view(&ui, metrics, &model, Lang::En);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安装失败的模型:可读文案 + 出口都有(不静默)。
|
||||
///
|
||||
/// 抓得住:失败态被画成「已完成」或什么都不给。
|
||||
/// 抓不住:失败原因的具体内容(core 只给中文 `message`,页面按规矩不画它,见模块文档)。
|
||||
#[test]
|
||||
fn a_failed_install_stays_visible() {
|
||||
let model = super::model::InstallView::of(
|
||||
&InstallProgressView {
|
||||
version: "1.21.9".to_string(),
|
||||
stage: InstallStage::Failed,
|
||||
..InstallProgressView::new()
|
||||
},
|
||||
Lang::En.catalog(),
|
||||
);
|
||||
assert!(model.failed());
|
||||
assert!(model.terminal());
|
||||
assert!(!model.running(), "失败之后不再给「取消安装」");
|
||||
assert_eq!(model.stage_label, "Install failed");
|
||||
}
|
||||
|
||||
/// 演示数据自己也得守规矩(它是出图的基线):抓中文字面量混入 / 行数与分类对不上。
|
||||
#[test]
|
||||
fn the_demo_data_is_sane() {
|
||||
let catalog = Lang::En.catalog();
|
||||
let demo = super::demo(catalog);
|
||||
assert!(demo.loaded);
|
||||
assert_eq!(demo.rows.len(), 6);
|
||||
assert_eq!(demo.category, Category::All, "混合列表没有单个分类该亮");
|
||||
assert!(
|
||||
demo.rows
|
||||
.iter()
|
||||
.all(|row| row.supporting.contains("released")),
|
||||
"英文目录下的副文案走英文 key"
|
||||
);
|
||||
assert!(
|
||||
demo.rows.iter().all(|row| !row.supporting.contains('版')),
|
||||
"英文目录下不许出现中文类型词"
|
||||
);
|
||||
}
|
||||
mod view;
|
||||
Reference in new issue
Block a user