fix(app-iced): 删掉空操作串行门 + 钉住档位贯穿产品启动路径 + 退场起点接当前进度
复审 r2 的三条: 1) 删掉 motion::serialize() 那把串行门:PUMP/LAST_FRAME 都是 thread_local,libtest 每用例 一线程 ⇒ 它改变不了任何结果(ULL_MOTION=off 下握门照样红)。留一个不起作用的同步原语 比没有更坏。subscription.rs 与 motion_sync.rs 的 idle_app 恢复原签名。 2) 真实根因是「喂给 configure 的值」:idle_app 先钉 STANDARD,随后 on_boot → configure_motion(app/update.rs:276)→ boot_env::motion_scale(..) 再定一次档,把钉住覆盖掉。 改成 BYPASS_PROBE(bool) → FORCED_SCALE(Cell<Option<MotionScale>>):configure 里钉住优先, 于是三条定档路径(configure_for_test / on_boot / boot_env 的 env·后端 tier)都穿不过去。 两条用例钉住它(纯合成 + 真 Message::Boot),删掉「钉住优先」两处都红。 3) P2-2:Modal::Closing 记住起手进度 from,progress = from*(1-eased) —— 进场途中被关掉时 从当前进度接着缩,不再先跳到 1.0(反向硬跳)。新用例 closing_during_the_entrance_does_not_jump_to_fully_open,把 from 改回 1.0 即红。
This commit is contained in:
1 parent
c21c6df51b
commit
787cb16189
4 files changed
+122
-44
No files matched your search
@@ -2,23 +2,16 @@
|
||||
//!
|
||||
//! ## 为什么这条不能在 `view` 里
|
||||
//!
|
||||
//! iced 0.14 的处理顺序是:
|
||||
//!
|
||||
//! ```text
|
||||
//! 消息 ──► update() ← program.subscription() 在这里求值(iced_winit src/lib.rs:1338)
|
||||
//! ──► build_user_interfaces() ← 这时才调 program.view(src/lib.rs:1233)
|
||||
//! ```
|
||||
//!
|
||||
//! 也就是说**订阅表的求值早于视图**。第一版把起跑写在 `view` 里,订阅表当拍看不见轨道,
|
||||
//! `frames()` 要等下一条消息才打开,而那一帧的 dt 是整段空闲 ⇒ 轨道被当成早已到期清掉,
|
||||
//! 用户看到的是一次 12/8px 的偏移闪跳(截图通道因为 `has_screenshot()` 恒真,**结构上**不可能
|
||||
//! 暴露这条)。所以起跑搬到这里:`App::update` 处理完每条消息后调 [`App::sync_motion`]。
|
||||
//! iced 0.14 的顺序:`update()`(**订阅表在这里求值**,iced_winit `src/lib.rs:1338`)→
|
||||
//! `build_user_interfaces()`(这时才调 `view`,`src/lib.rs:1233`)⇒ **订阅表的求值早于视图**。
|
||||
//! 第一版把起跑写在 `view` 里,订阅表当拍看不见轨道,`frames()` 要等下一条消息才打开,而那一帧
|
||||
//! 的 dt 是整段空闲 ⇒ 轨道被当成早已到期清掉,用户看到的是一次 12/8px 的偏移闪跳(截图通道
|
||||
//! 因为 `has_screenshot()` 恒真,**结构上**不可能暴露这条)。所以起跑搬到这里。
|
||||
//!
|
||||
//! ## 标识从哪来
|
||||
//!
|
||||
//! 从**路由**取(`AppState::route()` 是唯一真源):`screen_key` 说「归不归 pages 画」(账户页 /
|
||||
//! AI 独立页不归,它们之间切换不起跑),`nav_rank` 说「在**导航轨**上的左右次序」(方向只看它,
|
||||
//! 不看屏键大小——见 [`nav_rank`])。
|
||||
//! 从**路由**取(`AppState::route()` 是唯一真源):`screen_key` 说「归不归 pages 画」,
|
||||
//! `nav_rank` 说「在**导航轨**上的左右次序」(方向只看它,不看屏键大小——见 [`nav_rank`])。
|
||||
|
||||
use unlucky_presenter::router::Route;
|
||||
|
||||
@@ -153,10 +146,9 @@ fn screen_key(route: &Route) -> Option<u64> {
|
||||
/// 这一屏在**导航轨**上的次序(`NavItem::ALL` 的下标)——左右方向的唯一真源。
|
||||
///
|
||||
/// 为什么不能用屏键(`Screen::ALL` 的下标):两处顺序不同。`Screen::ALL` 是
|
||||
/// `Instances / Download / Content / Settings / Logs`,而导航轨是
|
||||
/// `Instances / Download / Logs / Settings / Ai` ⇒ 用屏键会把 **Logs↔Settings 的左右镜像**
|
||||
/// (复审 P2-1)。`Content` 与 `Download` 共用导航项,`rank` 相等,方向退回按屏键比大小
|
||||
/// (`Download ↔ Content` 判成同级水平位移)。
|
||||
/// `… Content / Settings / Logs`,而导航轨是 `… Logs / Settings / Ai` ⇒ 用屏键会把
|
||||
/// **Logs↔Settings 的左右镜像**(复审 P2-1)。`Content` 与 `Download` 共用导航项,`rank` 相等,
|
||||
/// 方向退回按屏键比大小(`Download ↔ Content` 判成同级水平位移)。
|
||||
///
|
||||
/// 账户头像(`NavItem::Account`)与 AI(`NavItem::Ai`)不在这个集合里 / 不归 pages 画,
|
||||
/// 返回 `None`——它们的切换本来就不起跑页面轨道。
|
||||
@@ -207,13 +199,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 有窗口、首屏动效已跑完、旧页模型已清空的 App。
|
||||
///
|
||||
/// **定档走 `configure_for_test`(复审 P2-5)**:产品入口 `configure` 会读宿主机「减少动效」
|
||||
/// 开关,用例在关了系统动画的机器上就会红。真探针的接线由 `motion::tests::modal` 里那条用例守。
|
||||
fn idle_app() -> (App, std::sync::MutexGuard<'static, ()>) {
|
||||
// **串行门**(复审 P2-5 第二半):动效全局态由本用例独占,别与并行用例互踩。
|
||||
let gate = motion::serialize();
|
||||
/// 有窗口、首屏动效已跑完、旧页模型已清空的 App。定档走 `configure_for_test`(P2-5,见
|
||||
/// `a_pinned_scale_survives_boot_with_motion_off`:它把档**钉住**,`on_boot` 的重定档赢不了)。
|
||||
fn idle_app() -> App {
|
||||
motion::reset();
|
||||
motion::configure_for_test(MotionScale::STANDARD, None);
|
||||
let mut app = headless();
|
||||
@@ -221,10 +209,9 @@ mod tests {
|
||||
let _ = app.update(Message::Boot);
|
||||
app.watch_until = None;
|
||||
run_until_idle();
|
||||
// 再走一条消息:`sync_motion` 在那一拍把启动期换页留下的 `previous_view` 烧掉。
|
||||
let _ = app.update(Message::Frame);
|
||||
let _ = app.update(Message::Frame); // 让 `sync_motion` 把启动期那次换页的旧页烧掉
|
||||
assert!(!motion::needs_frames(), "前提:首屏动效已经跑完");
|
||||
(app, gate)
|
||||
app
|
||||
}
|
||||
|
||||
/// 屏键 / 轨序两张表:**方向只听轨序**(复审 P2-1 的守卫)。
|
||||
@@ -278,7 +265,7 @@ mod tests {
|
||||
/// 旧版 `usize::from(is_some()) <= 1` 恒真。真要证明释放得靠析构计数插桩——**没做**。
|
||||
#[test]
|
||||
fn the_previous_page_model_is_dropped_and_never_accumulates() {
|
||||
let (mut app, _gate) = idle_app();
|
||||
let mut app = idle_app();
|
||||
assert!(app.previous_view.is_none(), "前提:静息态没有旧页");
|
||||
|
||||
let _ = app.update(Message::Page(pages::Message::Navigate(Route::Download)));
|
||||
@@ -310,7 +297,7 @@ mod tests {
|
||||
/// 画面上两个页面同时在动内容。抓不住:`States`(外观/组状态/设置快照)不随模型冻结(偏离 #4)。
|
||||
#[test]
|
||||
fn the_previous_page_is_a_frozen_snapshot() {
|
||||
let (mut app, _gate) = idle_app();
|
||||
let mut app = idle_app();
|
||||
let _ = app.update(Message::Page(pages::Message::Navigate(Route::Download)));
|
||||
let before = app
|
||||
.previous_view
|
||||
@@ -333,15 +320,15 @@ mod tests {
|
||||
|
||||
/// **退场窗口内那个框已经关掉、且不可再交互**(tower 的硬要求)。四件事:①逻辑状态当拍就空;
|
||||
/// ②画面还在退场(`dialog_model` 仍 `Some`);③150ms 内再点一次**什么都不发生**(`refreshes`
|
||||
/// 不涨 = `on_dialog` 的闸门);④**Esc 那条独立路径**也挡住(`escape` 的闸门)。
|
||||
/// **抓不住**:几何级命中(单测没有 iced 布局树,`zoom.rs` 也不改命中矩形)——那一层由闸门保证。
|
||||
/// 不涨 = `on_dialog` 的闸门);④**Esc 那条路径**也挡住(`escape` 的闸门)。**抓不住**:几何级
|
||||
/// 命中(单测没有 iced 布局树,`zoom.rs` 也不改命中矩形)——那一层由闸门保证。
|
||||
#[test]
|
||||
fn the_exiting_dialog_is_already_closed_and_swallows_clicks() {
|
||||
use crate::app::dialog::Message as DialogMessage;
|
||||
use crate::app::update::dialog::{escape, on_dialog};
|
||||
use unlucky_presenter::dialogs::{DialogKind, DialogSource};
|
||||
|
||||
let (mut app, _gate) = idle_app();
|
||||
let mut app = idle_app();
|
||||
let _ = app.state.dialogs.push(
|
||||
DialogSource::UserAction,
|
||||
"删除世界",
|
||||
@@ -381,4 +368,30 @@ mod tests {
|
||||
assert!(app.dialog_model.is_none(), "退场结束后不许留残影");
|
||||
assert!(!motion::needs_frames(), "退场结束后帧泵必须停");
|
||||
}
|
||||
|
||||
/// **钉住的档必须压过产品启动路径**(复审 r2 的根因守卫):走真 `Message::Boot` ——
|
||||
/// `on_boot` → `configure_motion` → `boot_env::motion_scale(..)`(本机 CPU 档 ⇒ HALF)→
|
||||
/// `configure`,全程都不许把测试钉住的 STANDARD 顶掉。
|
||||
///
|
||||
/// 为什么不去 `set_var("ULL_MOTION","off")`:那是**进程级写**,并行用例里别的线程正在
|
||||
/// `getenv`(`boot_env` 每次 Boot 都读),Linux 上这是真竞态。等价做法:把该 env 下
|
||||
/// `boot_env` 会产出的**取值**(`NONE`)直接喂给 `configure`——见
|
||||
/// `components::motion::tests::modal::a_pinned_scale_survives_every_later_configure`。
|
||||
///
|
||||
/// 抓得住:删掉 `configure` 里「`FORCED_SCALE` 优先」那段 ⇒ 本条红(r2 之前那 7 条红之一
|
||||
/// 的同一条路)。抓不住:`boot_env` 自己的分支表(env / 后端 tier 的规则,不在本 scope)。
|
||||
#[test]
|
||||
fn a_pinned_scale_survives_boot() {
|
||||
motion::reset();
|
||||
motion::configure_for_test(MotionScale::STANDARD, None);
|
||||
let mut app = headless();
|
||||
app.window = Some(iced::window::Id::unique());
|
||||
let _ = app.update(Message::Boot);
|
||||
assert_eq!(
|
||||
motion::scale(),
|
||||
MotionScale::STANDARD,
|
||||
"on_boot 的重定档不许顶掉测试钉住的档(r2 的根因)"
|
||||
);
|
||||
motion::reset();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user