fix(app-iced): M107 r1 复审五条 p2 —— 搜索与主版本桶一致、chips 可滚、单复数、端到端
复审 r1(p2-5items / fix-then-merge)的五条可达问题,逐条落点: - **P2-1** 搜索命中数与主版本桶不再自相矛盾:`group::active_bucket` 让第二级在搜索期间 **自动放宽到「全部」**(选择本身留在模型里,清空搜索后回来),侧栏点亮「全部版本」⇒ 没有幽灵选中; `rebuild_after_query` 回填时校验桶仍存在(新桶列表里没有的选择直接丢弃)。 判据:`a_search_hit_never_lands_on_an_empty_list`(4 分类 × 5 桶值 × 4 查询词的矩阵)+ 出图 11 - **P2-2** chips 档的主版本行套**横向滚动**(显式滚动条),竖栏与 chips 吃同一份 `major_entries`; 22 桶夹具断言最后一个桶 `1.0` 在列表里,出图 12 看得到滚动条 - **P2-3** 只做搜索这条路径(`rebuild_after_query` + 校验 + app 层测试);`app/state.rs::refresh_view` 的全量修法**留到 M105 之后**(不在 scope,且 M105 正改同一片文件)——报告里明写 - **P2-4** `1 matches` 修掉:新增 `VersionSearchHitsOne` / `VersionSearchElsewhereOne` 两条单数 key, 「在别处」用独立标题(不再拿命中数当标题,同屏一个数只报一次) - **P2-5** 搜索链路补 4 条 **app 层端到端**(键入 → 快照 → 重建 → 列表);变异自证: 删 `self.state.download_query = query` ⇒ 2 条红;`from_snapshot` 忽略 query ⇒ 8 条红; 去掉 `active_bucket` 的放宽 ⇒ 4 条红 - 顺带:64 宽图标条每项挂 tooltip(复审建议);`install.rs` 那段自相矛盾的文档订正 门禁:fmt 0 / app-iced lib 431 passed / workspace --exclude unlucky-app 全绿(见报告)
This commit is contained in:
1 parent
89a51a63b1
commit
45544791e5
11 files changed
+622
-56
No files matched your search
@@ -191,3 +191,138 @@ impl App {
|
||||
self.view.download.type_rail_folded = folded;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! 搜索链路的**端到端**断言(复审 P2-5)。
|
||||
//!
|
||||
//! 为什么非要在 app 层写:这条链是 `Message → on_page → on_download → AppState.download_query
|
||||
//! → refresh_view → from_snapshot 过滤`,中间任何一环断掉,页面层测试都看不见(页面只拿到
|
||||
//! 已经过滤好的模型)。复审原话:「把 `rebuild_after_query` 第一行删掉,现有测试全绿,而搜索
|
||||
//! 变成假控件」——这几条就是补那个洞的。
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use unlucky_presenter::dto::{AppEvent, Backend, CoreEvent, VersionKind, VersionSummary};
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 只记账、不回事件的后端(同 `app/update/download_wiring.rs`):命令出了 UI 层,状态一个字节
|
||||
/// 都不许变——否则「本地乐观回显」与「事件回流」终态相同,断言会假绿。
|
||||
struct Recorder(Rc<RefCell<Vec<AppCommand>>>);
|
||||
|
||||
impl Backend for Recorder {
|
||||
fn drain_events(&mut self) -> Vec<AppEvent> {
|
||||
Vec::new()
|
||||
}
|
||||
fn dispatch(&mut self, command: AppCommand) {
|
||||
self.0.borrow_mut().push(command);
|
||||
}
|
||||
}
|
||||
|
||||
fn version(id: &str) -> VersionSummary {
|
||||
VersionSummary {
|
||||
id: id.to_string(),
|
||||
kind: VersionKind::Release,
|
||||
released: "2026-09-20".to_string(),
|
||||
installed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个装好三行版本的 App(`1.21.9` / `1.20.4` / `1.20.1`)。
|
||||
fn app() -> App {
|
||||
let families = crate::theme::Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
let mut app = App::with_presenter(
|
||||
crate::cli::Options::default(),
|
||||
families,
|
||||
SchemeFlavor::Light,
|
||||
Box::new(Recorder(Rc::new(RefCell::new(Vec::new())))),
|
||||
SettingsBridge::memory_only(),
|
||||
);
|
||||
app.state
|
||||
.apply(AppEvent::Core(CoreEvent::VersionListChanged(vec![
|
||||
version("1.21.9"),
|
||||
version("1.20.4"),
|
||||
version("1.20.1"),
|
||||
])));
|
||||
app.refresh_view();
|
||||
app
|
||||
}
|
||||
|
||||
/// 键入 → 快照 → 列表:一条走完的链。
|
||||
///
|
||||
/// 抓得住:`rebuild_after_query` 不再写 `AppState.download_query`(搜索变假控件)、
|
||||
/// 或 `from_snapshot` 不再按 query 过滤(列表不跟着收窄)。
|
||||
/// 抓不住:真机上 `text_input` 的按键是否走到这里(那是组件的事,`search_bar` 已接 `on_input`)。
|
||||
#[test]
|
||||
fn typing_a_query_reaches_the_state_and_narrows_the_list() {
|
||||
let mut app = app();
|
||||
assert_eq!(app.view.download.rows.len(), 3, "初始是全量");
|
||||
|
||||
let _ = app.on_page(pages::Message::SetVersionQuery("1.20".to_string()));
|
||||
|
||||
assert_eq!(app.state.download_query, "1.20", "查询词落在快照里");
|
||||
assert_eq!(app.view.download.rows.len(), 2, "列表收窄到两行");
|
||||
assert_eq!(app.view.download.hits, 2);
|
||||
assert_eq!(app.view.download.query, "1.20", "模型回显查询词");
|
||||
|
||||
// 清空回到全量(`ClearVersionQuery` 那一支)
|
||||
let _ = app.on_page(pages::Message::ClearVersionQuery);
|
||||
assert!(app.state.download_query.is_empty());
|
||||
assert_eq!(app.view.download.rows.len(), 3);
|
||||
}
|
||||
|
||||
/// 查询词**跨事件存活**:再来一次重建,列表仍是收窄的。
|
||||
///
|
||||
/// 抓得住:把查询词只写在视图模型里(下一次 `refresh_view` 就丢)——这是「搜索看起来能用、
|
||||
/// 一来事件就复位」的坏法。
|
||||
#[test]
|
||||
fn the_query_survives_a_snapshot_rebuild() {
|
||||
let mut app = app();
|
||||
let _ = app.on_page(pages::Message::SetVersionQuery("1.21".to_string()));
|
||||
assert_eq!(app.view.download.rows.len(), 1);
|
||||
|
||||
// 安装进度这类事件回来时,`pump()` 会重建视图:查询词必须还在
|
||||
app.state
|
||||
.apply(AppEvent::Core(CoreEvent::InstallProgress(None)));
|
||||
app.refresh_view();
|
||||
|
||||
assert_eq!(app.state.download_query, "1.21", "查询词在快照里,重建不丢");
|
||||
assert_eq!(app.view.download.rows.len(), 1, "列表仍收窄");
|
||||
}
|
||||
|
||||
/// 搜索时侧栏的**位置与折叠**不许丢(`rebuild_after_query` 的三行)。
|
||||
///
|
||||
/// 抓得住:删掉那两行回填(敲第一个字符,类型栏自己弹开、主版本跳回「全部」)。
|
||||
/// 抓不住:其它事件(安装进度)回来时也会复位 —— 那条要 `app/state.rs` 的 `refresh_view`
|
||||
/// 一起改,不在本 mission scope,已在报告里明确登记「留到 M105 之后」。
|
||||
#[test]
|
||||
fn a_query_keeps_the_rail_position_and_the_fold() {
|
||||
let mut app = app();
|
||||
app.view.download.selected_major = Some("1.20".to_string());
|
||||
app.view.download.type_rail_folded = true;
|
||||
|
||||
let _ = app.on_page(pages::Message::SetVersionQuery("1.20".to_string()));
|
||||
|
||||
assert_eq!(
|
||||
app.view.download.selected_major.as_deref(),
|
||||
Some("1.20"),
|
||||
"桶选择还在(生效值在搜索期间放宽,但选择本身不许被清)"
|
||||
);
|
||||
assert!(app.view.download.type_rail_folded, "折叠状态还在");
|
||||
|
||||
// 选中一个**新桶列表里不存在**的桶:搜索后不许把它复活成幽灵选中(P2-1(a))
|
||||
app.view.download.selected_major = Some("9.99".to_string());
|
||||
let _ = app.on_page(pages::Message::SetVersionQuery("1.20".to_string()));
|
||||
assert_eq!(
|
||||
app.view.download.selected_major, None,
|
||||
"新桶列表里没有 9.99 ⇒ 回填空值,而不是留一个点不亮的幽灵选中"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user