feat(app-iced): M101 r2 设置页状态漏斗 + pages/mod.rs 拆分 + r1 复审订正
r1 复审的 P1:消息漏斗只解决了「加一支消息」,一组要画真值仍要改 `settings/mod.rs` (`Group.body` 与 `view` 只认 `appearance::State`)。本 commit 补齐与消息漏斗同构的状态漏斗: - 新增 `pages/settings/state.rs`:`Groups`(一个组一个字段,字段类型在组自己文件里)+ `States`(按值传给渲染器的只读视图:外观状态 + 组状态)+ `demo_groups()`(harness 的 `'static` 默认状态)。 - `Group.body` 签名改成 `for<'a> fn(&'a Ui, States<'a>) -> Option<Element>`:每组自己从 `States` 取自己要的数据,「没有可画的状态」返回 `None` 由 `form` 退回演示行。 `appearance` 组照旧接 `pages/appearance/section.rs`(M93 路径行为不变)。 - `App.settings: pages::settings::Groups`(`app/state.rs` 字段列表末尾)+ `app/view.rs` 构造 `States` 走新入口 `pages::page_with_settings`。加/改某一组的字段只改那一组的文件。 - 四个组各自的 `State` 落在各自文件;`java::Message` 留一支只有测试构造的 `Ping`,配 `app/update/settings/tests.rs` 的 `a_group_message_reaches_its_handler_through_the_funnel` ——消息漏斗第一次有**运行时**证据。 r1 其余条目: - P2-1 删掉 `only_the_appearance_group_carries_a_real_control_body`(被四条 per-group 断言蕴含), 并在同处写明删掉之后「唯一性」没人守;三份 section 文档从「改 3 个文件」订正为 2 个。 - P2-2 `pub mod settings` → `pub(crate) mod settings`(四个消息口模块 `pub(crate)`), 并订正两处理由(lint 按类型自身可见性判,不是可达性)。 - P2-3 `pages/mod.rs` 397 行 → 拆出 `pages/message.rs` / `pages/screen.rs`(纯搬移,`pub use` 保证 `pages::Message` / `Action` / `Screen` 路径不变),并删掉 M101 自己那 18 行重复文档。 - P3-1 订正共享点清单(`Key` 由宏从域文件累积生成,加 key 不改 `catalog.rs`)。 - P3-2 守门改成「除已知臂外任何含 `=>` 的行都算兜底」(`other =>` 也拦),并加「恰好四臂」断言。 - P3-3 `ai.rs` → `ai_about.rs` 单独一条 R100 改名提交,`git log --follow` 不断链。 - P3-4 `message.rs` 的「加一支消息改 1 个文件」订正为 2 个。
This commit is contained in:
1 parent
423ea77758
commit
029ece3f17
25 files changed
+1004
-240
No files matched your search
@@ -0,0 +1,149 @@
|
||||
//! 设置页分组消息分发的**守门测试**(M101 `SETTINGSMSG`)。
|
||||
//!
|
||||
//! 判据与 `app/update/page_messages.rs` 里那条 `the_page_match_has_no_wildcard_fallback` 同源:
|
||||
//! 兜底会把「加了变体忘了接线」从**编译错误**降级成「点了没反应」。两级 dispatch 各守一条:
|
||||
//! 那一层守 `pages::Message`,本文件守 `GroupMessage` 与四个 handler。
|
||||
//!
|
||||
//! r1 复审的 P3-2 指出上一版有洞:只拦 `_ =>` 的话,`other => { … }` 这种 **binding 兜底**
|
||||
//! 照样取消穷尽性、照样静默。本版判据反过来写——**除已知臂外,任何含 `=>` 的行都算兜底**:
|
||||
//! `settings.rs` 里每条臂都必须以 `pages::settings::GroupMessage::` 开头且恰好 4 条;handler 里
|
||||
//! 每条臂都必须以该组自己的变体路径开头;三个还是空枚举的 handler 一行 `=>` 都不许有。
|
||||
//!
|
||||
//! 站位:本文件是 `app::update::settings::tests`,`include_str!` 的路径相对本文件所在目录。
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::mock::MockBackend;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
|
||||
use crate::app::message::Message;
|
||||
use crate::app::state::App;
|
||||
use crate::cli::Options;
|
||||
use crate::pages;
|
||||
use crate::theme::Families;
|
||||
|
||||
/// 不碰磁盘的 App(mock 后端 + 内存设置桥),与 `app/tests.rs` 的 `headless` 同一副骨架。
|
||||
fn headless() -> App {
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
App::with_presenter(
|
||||
Options::default(),
|
||||
FAMILIES,
|
||||
SchemeFlavor::Light,
|
||||
Box::new(MockBackend::new()),
|
||||
SettingsBridge::memory_only(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 收集「不该存在的臂」:非注释行里出现 `=>`,但不以该文件允许的前缀开头。
|
||||
fn offending_arms(path: &str, source: &str, allowed: Option<&str>) -> Vec<String> {
|
||||
source
|
||||
.lines()
|
||||
.enumerate()
|
||||
.filter_map(|(index, line)| {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("//") || !trimmed.contains("=>") {
|
||||
return None;
|
||||
}
|
||||
match allowed {
|
||||
Some(prefix) if trimmed.starts_with(prefix) => None,
|
||||
_ => Some(format!("{path}:{}: {trimmed}", index + 1)),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `on_settings` 与四个 handler 里都**不许**出现任何形式的兜底臂(`_ =>` 或 `other =>`)。
|
||||
///
|
||||
/// 抓得住:① 给 `on_settings` 加 `_ =>` 或 `other =>`(`GroupMessage` 不再穷尽);
|
||||
/// ② 四个 handler 把 `match message {}` 改成 `_ => Task::none()` / `other => { … }`(同样取消穷尽性);
|
||||
/// ③ 给 settings.rs 多接一条不认识的臂(前缀不对)。
|
||||
/// 抓不住:handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样
|
||||
/// —— 类型对得上就编得过;也抓不住「新变体接了个空壳」。那两条只有人读能看得出来。
|
||||
#[test]
|
||||
fn the_group_dispatch_has_no_catch_all_arm() {
|
||||
let sources: [(&str, &str, Option<&str>); 5] = [
|
||||
(
|
||||
"app/update/settings.rs",
|
||||
include_str!("../settings.rs"),
|
||||
Some("pages::settings::GroupMessage::"),
|
||||
),
|
||||
(
|
||||
"app/update/settings/java.rs",
|
||||
include_str!("java.rs"),
|
||||
Some("pages::settings::java::Message::"),
|
||||
),
|
||||
(
|
||||
"app/update/settings/download.rs",
|
||||
include_str!("download.rs"),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"app/update/settings/language_appearance.rs",
|
||||
include_str!("language_appearance.rs"),
|
||||
None,
|
||||
),
|
||||
(
|
||||
"app/update/settings/ai_about.rs",
|
||||
include_str!("ai_about.rs"),
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
let mut offending = Vec::new();
|
||||
for (path, source, allowed) in sources {
|
||||
offending.extend(offending_arms(path, source, allowed));
|
||||
}
|
||||
|
||||
assert!(
|
||||
offending.is_empty(),
|
||||
"设置页分组消息的 dispatch 里出现了兜底臂({offending:?}):\
|
||||
任何 catch-all / binding 臂都会取消 `GroupMessage` 的穷尽性,\
|
||||
新变体漏接线时编译器不再报 E0004"
|
||||
);
|
||||
}
|
||||
|
||||
/// `on_settings` 必须**恰好四条**臂,一条一条显式写全(不是「至少」)。
|
||||
///
|
||||
/// 抓得住:有人删掉一条臂、或者把两条合到一起(`A(..) | B(..) =>` 这种也过不了上面那条前缀判据,
|
||||
/// 这里是第二条独立判据)。
|
||||
/// 抓不住:臂的顺序(顺序不影响语义)。
|
||||
#[test]
|
||||
fn the_group_dispatch_has_exactly_four_arms() {
|
||||
let source = include_str!("../settings.rs");
|
||||
let arms = source
|
||||
.lines()
|
||||
.filter(|line| line.trim().starts_with("pages::settings::GroupMessage::"))
|
||||
.count();
|
||||
|
||||
assert_eq!(
|
||||
arms, 4,
|
||||
"`on_settings` 应该恰好四条臂(java / download / language_appearance / ai_about),实际 {arms} 条"
|
||||
);
|
||||
}
|
||||
|
||||
/// **活的端到端证据**:一条组消息真的从外壳那条既有通道走到该组的 handler(r1 复审要求)。
|
||||
///
|
||||
/// 路径:`app::Message::Page` → `on_page` → `pages::Message::Settings` → `on_settings` →
|
||||
/// `on_settings_java`,最后改到 `App` 的状态。r1 之前这条链只有编译期证据(`GroupMessage` 是
|
||||
/// uninhabited,运行时是死代码);现在至少有一支真的跑得起来。
|
||||
///
|
||||
/// 抓得住:漏斗某一环接断了(`on_page` 没转交、`on_settings` 转错组、handler 忽略消息)——那时
|
||||
/// `view_dirty` 不会翻。抓不住:真实用户点击(本条没有 UI 控件,构造点只有测试)。
|
||||
#[test]
|
||||
fn a_group_message_reaches_its_handler_through_the_funnel() {
|
||||
let mut app = headless();
|
||||
app.view_dirty = false;
|
||||
|
||||
let _ = app.update(Message::Page(pages::Message::Settings(
|
||||
pages::settings::GroupMessage::Java(pages::settings::java::Message::Ping),
|
||||
)));
|
||||
|
||||
assert!(
|
||||
app.view_dirty,
|
||||
"java 线的消息没有走到 `on_settings_java`:消息漏斗在某一环断了"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user