From 7c27f7308c29f1eaf1ccaf8af3f43431b75ae44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 10 May 2026 01:22:30 -0400 Subject: [PATCH] Lock the input invariants with web_surface store regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five PerTabSurface tests pin the input contract so future refactors can't silently regress it: - typed_text_enters_pending_input_after_clicked_viewport: typing reaches the sidecar once a click establishes keyboard focus. - scroll_delta_enters_pending_input_after_wheel: scroll deltas combine across multiple wheel events. - viewport_size_changes_after_stable_second_measurement: viewport resize is debounced behind a "same size twice" guard. - scroll_after_click_keeps_keyboard_focus_and_typed_text: the bug fixed last commit cycle — scroll drops the buffered click point but must keep keyboard_focus and buffered keystrokes. - typing_without_a_prior_click_is_rejected: typing without focus returns false, so a future refactor can't accept stray keystrokes. No instrumentation eprintln/log added in production paths (per CLAUDE.md NO LOGGING). cargo test ely_app --bin ely_app: 102 passed. --- crates/ely_app/src/shell/web_surface_tests.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/ely_app/src/shell/web_surface_tests.rs b/crates/ely_app/src/shell/web_surface_tests.rs index c8b7d8a..c5426ef 100644 --- a/crates/ely_app/src/shell/web_surface_tests.rs +++ b/crates/ely_app/src/shell/web_surface_tests.rs @@ -49,6 +49,70 @@ fn viewport_size_changes_after_stable_second_measurement() -> Result<(), Box Result<(), Box> { + let mut store = WebSurfaceStore::new(); + let tab = web_tab("https://example.com/form")?; + let url = tab.url().as_str(); + + assert!(store.record_viewport_size(tab.id(), web_bounds())); + assert!(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)))); + assert!(store.record_typed_text(tab.id(), url, "h")); + + assert!(store.record_scroll_delta(tab.id(), url, point(px(0.0), px(140.0)))); + + assert!( + store.record_typed_text(tab.id(), url, "i"), + "scroll must not erase keyboard focus — typing after a scroll should still buffer", + ); + + let input = store.take_pending_input(tab.id(), url); + + assert_eq!( + input.scroll_delta.map(|delta| (delta.x(), delta.y())), + Some((0, 140)), + "scroll delta should reach the sidecar", + ); + assert_eq!(input.scroll_offset.y(), 140); + assert!( + input.click_point.is_none(), + "post-scroll click coordinates would land on the wrong DOM node — they must be dropped", + ); + assert_eq!( + input.typed_text.as_deref(), + Some("hi"), + "buffered keystrokes from before AND after the scroll must reach the sidecar", + ); + Ok(()) +} + +/// Locks the precondition that `record_typed_text` requires a prior +/// click to have established keyboard focus. Without this guard, a +/// future refactor could silently start buffering stray keystrokes +/// against an unfocused tab — and the user would see characters land +/// on whichever DOM node Servo last focused, with no visible cause. +#[test] +fn typing_without_a_prior_click_is_rejected() -> Result<(), Box> { + let mut store = WebSurfaceStore::new(); + let tab = web_tab("https://example.com/form")?; + let url = tab.url().as_str(); + + assert!(store.record_viewport_size(tab.id(), web_bounds())); + assert!( + !store.record_typed_text(tab.id(), url, "x"), + "typing must fail until a click establishes keyboard focus on this tab and url", + ); + Ok(()) +} + fn web_bounds() -> Bounds { Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0))) }