feat(app-iced,pages): M100 取证密闭 + harness 参数拆分 + 设置页测试按 section 下放
四件前置事,都是为了设置页正文四条 section 线能并行(不写任何 section 正文):
1. 取证密闭(两条通道各有各的机制,证据分开算)
- 产品 bin:`cli::ForcedState` 加 `Rest`(静息态),给了 `--screenshot` 且没显式
`--force-state` 时默认 `Some(Rest)`;交互模式仍是 `None`(`shell::overridden`
与 `app/view.rs` 的 `model()` 各补一支)。
- harness:截图窗口开 `window::enable_mouse_passthrough`(winit 的
`set_cursor_hittest(false)`)⇒ 窗口收不到指针事件 ⇒ 全树控件恒为 Active。
为什么用通道级而不是逐控件灌 forced:设置页「外观」区的滑块/文本框/裸 button 都不消费
forced,逐控件做要改十几个组件文件,而新产品组件随时会漏。
- harness 同时数指针事件并打进日志(`指针事件=N`):让「这张图见过几个鼠标事件」变成
可读数字,替代「连跑几次 hash 一样」那种恒真的假绿。
2. harness 的外观真状态入口:`--appearance=real` 让设置页走 `appearance: Some(..)`
(工厂态 + `ULL_APPEARANCE_*`),新入口 `pages::app_with_appearance`。
`--screen=settings` 之前就有了,但 `appearance` 恒为 None,D30 要的那一屏根本不存在。
3. `pages/harness.rs` 400 行 → 拆 `pages/harness/request.rs`(`#[path]` 范式),
纯搬移部分 240 行 + 新增密闭代码。拆分等价性:同一 argv 的 PNG sha256 与拆分前一致。
4. `pages/settings/tests.rs` 按 section 下放成 `tests/{general,appearance,java,download}_section.rs`,
mod 层只留跨 section 断言(每条写清守什么、为什么不能下放)。
给某组加一条新断言 = 只改它自己那个文件。
顺带:`shell/mod.rs` 在 HEAD 就 399 行、`cli.rs` 394 行,两处都按同一范式把 tests 搬成
`shell/tests.rs` / `cli/tests.rs`(断言一字不动,逐字节可 diff)。
变异验证:把四处实现各改坏一处(默认 Rest、passthrough 默认值、overridden 的 Rest 分支、
view 的 Rest 映射),对应 4 条断言全红(390 passed / 4 failed),恢复后 394 passed。
This commit is contained in:
1 parent
2ad808d55c
commit
2061296615
16 files changed
+1152
-469
No files matched your search
@@ -309,6 +309,9 @@ pub(super) fn label<'a>(
|
||||
/// 取证用的状态覆盖(`--force-state`):只改画出来的状态,不动任何状态机。
|
||||
pub(super) fn overridden(forced: Option<ForcedState>, status: button::Status) -> button::Status {
|
||||
match forced {
|
||||
// `Rest` = 取证图的常态:**明确**画成 Active,把系统光标带来的 hover/pressed 压掉。
|
||||
// 截图通道默认就是它(见 `cli::Options::parse` 的尾部),否则同一份代码会因指针位置出不同的图。
|
||||
Some(ForcedState::Rest) => button::Status::Active,
|
||||
Some(ForcedState::Hover) => button::Status::Hovered,
|
||||
Some(ForcedState::Pressed) => button::Status::Pressed,
|
||||
None => status,
|
||||
@@ -323,77 +326,5 @@ pub(super) fn filled(color: Color) -> container::Style {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
fn ui(flavor: SchemeFlavor) -> Ui {
|
||||
Ui::with_platform(
|
||||
flavor,
|
||||
Platform::Windows,
|
||||
Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_screen_only_uses_four_text_sizes_at_most() {
|
||||
let ui = ui(SchemeFlavor::Dark);
|
||||
let card = Component::OutlinedCard.spec();
|
||||
let sizes = [
|
||||
ui.emphasized(TypeRole::TitleLarge).size_px,
|
||||
ui.emphasized(TypeRole::LabelLarge).size_px,
|
||||
ui.emphasized(card.text).size_px,
|
||||
ui.style(card.support.unwrap_or(card.text)).size_px,
|
||||
ui.style(TypeRole::LabelMedium).size_px,
|
||||
];
|
||||
let mut unique = sizes.to_vec();
|
||||
unique.sort_by(f32::total_cmp);
|
||||
unique.dedup();
|
||||
assert!(
|
||||
unique.len() <= 4,
|
||||
"rubric D4:一屏字号档 ≤4(实际 {} 档 {unique:?})",
|
||||
unique.len()
|
||||
);
|
||||
assert!(unique[0] >= 12.0, "最小档不下 12px(caption ≥11)");
|
||||
assert!(unique[unique.len() - 1] <= 28.0, "列表页标题不得 >28px");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flavor_slug_is_stable_for_filenames() {
|
||||
assert_eq!(crate::shot::flavor_slug(&ui(SchemeFlavor::Light)), "light");
|
||||
assert_eq!(crate::shot::flavor_slug(&ui(SchemeFlavor::Dark)), "dark");
|
||||
}
|
||||
|
||||
/// M86:标题栏 caption 图标缩到 20 后,**容器一个数都没动**(46×48),图标也还在框里。
|
||||
#[test]
|
||||
fn caption_icon_shrinks_without_touching_the_container() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
let metrics = ui.metrics();
|
||||
let caption = icons::CAPTION_PX;
|
||||
assert_eq!(metrics.titlebar_button_width, 46.0, "容器宽不许跟着图标改");
|
||||
assert_eq!(metrics.titlebar_height, 48.0, "容器高不许跟着图标改");
|
||||
assert!(
|
||||
caption < metrics.titlebar_button_width,
|
||||
"图标 {caption} 必须小于按钮宽 {}",
|
||||
metrics.titlebar_button_width
|
||||
);
|
||||
assert!(
|
||||
caption < metrics.titlebar_height,
|
||||
"图标 {caption} 必须小于按钮高 {}",
|
||||
metrics.titlebar_height
|
||||
);
|
||||
// 官方口径 24 仍在令牌里(`IconSize::Toolbar`),偏离只发生在 caption 这一个入口。
|
||||
assert!(
|
||||
caption < unlucky_design::icons::IconSize::Toolbar.px(),
|
||||
"caption 图标必须小于官方 24(缩小是用户点名的)"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,126 @@
|
||||
//! `shell` 的断言(M100 从 `shell/mod.rs` 搬出来:那边在 HEAD 上已经 **399 行**,
|
||||
//! 加一个 `ForcedState::Rest` 分支就必破 400 行硬线)。
|
||||
//!
|
||||
//! 搬移**不改一行断言**:只去掉了 `mod tests { }` 那一层缩进,外加 M100 新增的两条
|
||||
//! 取证不变量(见文件末尾)。
|
||||
//!
|
||||
//! 站位:本文件是 `crate::shell::tests`,`use super::*;` 就够。
|
||||
use super::*;
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
fn ui(flavor: SchemeFlavor) -> Ui {
|
||||
Ui::with_platform(
|
||||
flavor,
|
||||
Platform::Windows,
|
||||
Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_screen_only_uses_four_text_sizes_at_most() {
|
||||
let ui = ui(SchemeFlavor::Dark);
|
||||
let card = Component::OutlinedCard.spec();
|
||||
let sizes = [
|
||||
ui.emphasized(TypeRole::TitleLarge).size_px,
|
||||
ui.emphasized(TypeRole::LabelLarge).size_px,
|
||||
ui.emphasized(card.text).size_px,
|
||||
ui.style(card.support.unwrap_or(card.text)).size_px,
|
||||
ui.style(TypeRole::LabelMedium).size_px,
|
||||
];
|
||||
let mut unique = sizes.to_vec();
|
||||
unique.sort_by(f32::total_cmp);
|
||||
unique.dedup();
|
||||
assert!(
|
||||
unique.len() <= 4,
|
||||
"rubric D4:一屏字号档 ≤4(实际 {} 档 {unique:?})",
|
||||
unique.len()
|
||||
);
|
||||
assert!(unique[0] >= 12.0, "最小档不下 12px(caption ≥11)");
|
||||
assert!(unique[unique.len() - 1] <= 28.0, "列表页标题不得 >28px");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flavor_slug_is_stable_for_filenames() {
|
||||
assert_eq!(crate::shot::flavor_slug(&ui(SchemeFlavor::Light)), "light");
|
||||
assert_eq!(crate::shot::flavor_slug(&ui(SchemeFlavor::Dark)), "dark");
|
||||
}
|
||||
|
||||
/// **交互模式不许被取证开关污染**(M100 的头号不变量)。
|
||||
///
|
||||
/// 抓得住:谁把 `--screenshot` 的默认值 `Some(Rest)` 顺手设成了 `Options::force_state` 的默认值
|
||||
/// (或者把默认往下挪一层,落到 `overridden` 的 `None` 分支上)——那会让**用户窗口里所有按钮
|
||||
/// 永远不显示 hover**,是产品事故,而不是取证问题。
|
||||
/// 抓不住:取证模式有没有真的压掉 hover(那是下一条)。
|
||||
#[test]
|
||||
fn interactive_mode_still_hovers() {
|
||||
use iced::widget::button::Status;
|
||||
|
||||
for status in [Status::Hovered, Status::Pressed, Status::Active] {
|
||||
assert_eq!(
|
||||
overridden(None, status),
|
||||
status,
|
||||
"交互模式(None)必须跟控件自己的状态走,不许替用户钉死"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **取证图的常态钉死**:`Rest` 把控件压成 `Active`,不管它自己的运行期状态是什么。
|
||||
///
|
||||
/// 抓得住:谁把 `Rest` 接成「保留现状」(那等于没钉:系统指针 hover 照样漏进图——M99 复审实测
|
||||
/// 连跑 8 次翻盘 1 次就是这里没钉住)。
|
||||
/// 抓不住:① 不经过 `overridden` 的控件(标题栏以外的裸 `iced::button`,例如设置页「外观」区);
|
||||
/// ② 窗口到底还收不收鼠标事件(那是 harness 的 passthrough,靠出图日志的 `指针事件=N` 自证)。
|
||||
#[test]
|
||||
fn rest_suppresses_cursor_hover() {
|
||||
use iced::widget::button::Status;
|
||||
|
||||
for status in [Status::Hovered, Status::Pressed, Status::Active] {
|
||||
assert_eq!(
|
||||
overridden(Some(ForcedState::Rest), status),
|
||||
Status::Active,
|
||||
"Rest 必须压掉 {status:?}"
|
||||
);
|
||||
}
|
||||
// 明细态照旧(D30 的状态矩阵图靠它们)
|
||||
assert_eq!(
|
||||
overridden(Some(ForcedState::Hover), Status::Active),
|
||||
Status::Hovered
|
||||
);
|
||||
assert_eq!(
|
||||
overridden(Some(ForcedState::Pressed), Status::Active),
|
||||
Status::Pressed
|
||||
);
|
||||
}
|
||||
|
||||
/// M86:标题栏 caption 图标缩到 20 后,**容器一个数都没动**(46×48),图标也还在框里。
|
||||
#[test]
|
||||
fn caption_icon_shrinks_without_touching_the_container() {
|
||||
let ui = ui(SchemeFlavor::Light);
|
||||
let metrics = ui.metrics();
|
||||
let caption = icons::CAPTION_PX;
|
||||
assert_eq!(metrics.titlebar_button_width, 46.0, "容器宽不许跟着图标改");
|
||||
assert_eq!(metrics.titlebar_height, 48.0, "容器高不许跟着图标改");
|
||||
assert!(
|
||||
caption < metrics.titlebar_button_width,
|
||||
"图标 {caption} 必须小于按钮宽 {}",
|
||||
metrics.titlebar_button_width
|
||||
);
|
||||
assert!(
|
||||
caption < metrics.titlebar_height,
|
||||
"图标 {caption} 必须小于按钮高 {}",
|
||||
metrics.titlebar_height
|
||||
);
|
||||
// 官方口径 24 仍在令牌里(`IconSize::Toolbar`),偏离只发生在 caption 这一个入口。
|
||||
assert!(
|
||||
caption < unlucky_design::icons::IconSize::Toolbar.px(),
|
||||
"caption 图标必须小于官方 24(缩小是用户点名的)"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user