audit combobox

This commit is contained in:
2026-11-08 11:00:39 +07:00
parent 21d32736a8
commit abb56567e4
7 changed files with 315 additions and 257 deletions
+27 -2
View File
@@ -2,8 +2,8 @@ use std::cell::RefCell;
use std::rc::Rc;
use gpui::{
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
Styled as _, TestAppContext, VisualTestContext, Window, div,
AppContext as _, Context, Entity, IntoElement, MouseButton, ParentElement as _, Render,
SharedString, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px,
};
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
use gpui_native_core::init;
@@ -99,6 +99,31 @@ fn draw(visual: &mut VisualTestContext) {
});
}
fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
visual.simulate_mouse_down(position, MouseButton::Left, gpui::Modifiers::default());
visual.simulate_mouse_up(position, MouseButton::Left, gpui::Modifiers::default());
}
/// Clicking into a valueless combobox field opens the options right away:
/// no keystroke is needed to see the choices.
#[test]
fn clicking_the_empty_field_opens_the_popup() {
let mut host = combo_host(false, false);
// Inside the field, which sits at the top of the padded host.
click(&mut host.visual, point(px(100.), px(27.)));
draw(&mut host.visual);
assert!(
host.state.read_with(&host.cx, |state, _| state.is_open()),
"clicking the empty field should show the options"
);
assert!(
host.visual.debug_bounds("test-combo-panel").is_some(),
"the options panel should be rendered"
);
}
fn focus(host: &mut ComboHost) {
let state = host.state.clone();
host.visual.update(|window, cx| {
+22 -7
View File
@@ -38,8 +38,11 @@ impl Render for FieldHost {
}
}
/// A field page in a real window, with the url input focused.
fn field_host() -> (
/// A field page in a real window; the url input is focused unless
/// `focus` is false, which leaves the page truly idle.
fn field_host(
focus: bool,
) -> (
TestAppContext,
Entity<InputState>,
Rc<RefCell<usize>>,
@@ -64,8 +67,10 @@ fn field_host() -> (
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
let focus = url.read(cx).focus_handle();
window.focus(&focus, cx);
if focus {
let focus = url.read(cx).focus_handle();
window.focus(&focus, cx);
}
window.draw(cx).clear(cx);
});
(cx, url, renders, visual)
@@ -94,17 +99,27 @@ fn type_and_time(visual: &mut VisualTestContext, text: &str) -> std::time::Durat
#[test]
fn an_idle_field_page_schedules_no_frames() {
let (_cx, _url, _renders, mut visual) = field_host();
let (_cx, _url, _renders, mut visual) = field_host(false);
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
assert_eq!(
frames, 0,
"an idle field page must not request animation frames"
"an idle unfocused field page must not request animation frames"
);
}
#[test]
fn a_focused_field_page_schedules_frames_for_its_blink() {
let (_cx, _url, _renders, mut visual) = field_host(true);
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
assert!(
frames > 0,
"the focused caret's blink needs frames to redraw at the blink cadence"
);
}
#[test]
fn field_typing_frame_cost() {
let (_cx, _url, _renders, mut visual) = field_host();
let (_cx, _url, _renders, mut visual) = field_host(true);
let elapsed = type_and_time(&mut visual, "dsdsadsadadadada");
assert!(
elapsed.as_millis() < 1000,
+27 -24
View File
@@ -269,42 +269,45 @@ fn read_only_refuses_edits_but_keeps_selection_and_copy() {
assert_eq!(clipboard, "lo");
// The caret is suppressed entirely while read-only.
let caret = cx.update(|cx| state.update(cx, |state, cx| state.caret_visible(true, cx)));
let caret = cx.update(|cx| state.update(cx, |state, _| state.caret_visible(true)));
assert!(!caret);
}
#[test]
fn caret_blinks_while_focused_and_holds_solid_while_typing() {
fn the_caret_visibility_is_pure_time_math_against_the_epoch() {
let (cx, state, mut visual) = input_window("", false);
focus(&mut visual, &state);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
visual.run_until_parked();
let caret = || cx.update(|cx| state.update(cx, |state, _| state.caret_visible(true)));
let caret = || cx.update(|cx| state.update(cx, |state, cx| state.caret_visible(true, cx)));
// The caret shows on focus, hides after one interval, and reappears
// after another.
assert!(caret());
cx.executor().advance_clock(Duration::from_millis(500));
visual.run_until_parked();
assert!(!caret());
cx.executor().advance_clock(Duration::from_millis(500));
visual.run_until_parked();
// Fresh focus starts a new epoch: the caret shows solid — the elapsed
// time since the epoch began is microseconds, far under one interval.
assert!(caret());
// Typing holds the caret solid and restarts the cycle.
// Typing restarts the epoch, so the caret stays solid through edits.
visual.simulate_keystrokes("a");
assert!(caret());
cx.executor().advance_clock(Duration::from_millis(500));
visual.run_until_parked();
assert!(!caret());
// An unfocused field reports no caret.
let caret_unfocused =
cx.update(|cx| state.update(cx, |state, cx| state.caret_visible(false, cx)));
assert!(!caret_unfocused);
// The blink cadence itself: hidden in the second half of the cycle.
let hidden = cx.update(|cx| {
state.update(cx, |state, _| {
state.caret_blink_visible_at(true, Duration::from_millis(700))
})
});
assert!(!hidden);
let visible = cx.update(|cx| {
state.update(cx, |state, _| {
state.caret_blink_visible_at(true, Duration::from_millis(300))
})
});
assert!(visible);
// An unfocused field reports no caret, at any point in the cycle.
let unfocused = cx.update(|cx| {
state.update(cx, |state, _| {
state.caret_blink_visible_at(false, Duration::from_millis(100))
})
});
assert!(!unfocused);
}
#[test]