chore: audit all components (#2)
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
+190
-64
@@ -3,12 +3,15 @@ use gpui::{
|
||||
MouseUpEvent, ParentElement as _, Render, ScrollDelta, ScrollEvent, Styled as _,
|
||||
TestAppContext, VisualTestContext, Window, div, point, px,
|
||||
};
|
||||
use gpui_native_core::button::{Button, ButtonVariant};
|
||||
use gpui_native_core::common::Sizable as _;
|
||||
use gpui_native_core::input::{
|
||||
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, SubmitOn,
|
||||
bind_input_keys,
|
||||
};
|
||||
use gpui_native_theme::Theme;
|
||||
use std::cell::RefCell;
|
||||
use gpui_native_icons::IconRole;
|
||||
use gpui_native_theme::{BORDER_WIDTH, Size, Theme};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -266,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]
|
||||
@@ -459,15 +465,6 @@ fn disabled_input_ignores_typing_and_clicks() {
|
||||
assert_eq!(value(&cx, &state), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_value_replaces_the_buffer() {
|
||||
let (cx, state, mut visual) = input_window("", false);
|
||||
visual.update(|window, cx| {
|
||||
state.update(cx, |state, cx| state.set_value("preset", window, cx));
|
||||
});
|
||||
assert_eq!(value(&cx, &state), "preset");
|
||||
}
|
||||
|
||||
/// Subscribes to `state`'s events, recording each variant into a sink that
|
||||
/// lives for the rest of the test. The returned `Subscription` must stay
|
||||
/// bound for the same lifetime — dropping it unsubscribes.
|
||||
@@ -536,26 +533,6 @@ fn set_value_silent_updates_without_emitting() {
|
||||
assert_eq!(count(&sink, &InputStateEvent::TextChanged), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_and_blur_emit_events() {
|
||||
let (mut cx, state, mut visual) = input_window("", false);
|
||||
let (sink, _subscription) = record_events(&mut cx, &state);
|
||||
|
||||
focus(&mut visual, &state);
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
visual.run_until_parked();
|
||||
assert_eq!(count(&sink, &InputStateEvent::Focused), 1);
|
||||
|
||||
visual.update(|window, cx| window.blur(cx));
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
visual.run_until_parked();
|
||||
assert_eq!(count(&sink, &InputStateEvent::Blurred), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasted_newlines_become_spaces() {
|
||||
let (cx, state, mut visual) = input_window("", false);
|
||||
@@ -568,15 +545,6 @@ fn pasted_newlines_become_spaces() {
|
||||
assert_eq!(value(&cx, &state), "a bc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_value_sanitizes_newlines() {
|
||||
let (cx, state, mut visual) = input_window("", false);
|
||||
visual.update(|window, cx| {
|
||||
state.update(cx, |state, cx| state.set_value("a\nb", window, cx));
|
||||
});
|
||||
assert_eq!(value(&cx, &state), "a b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_emits_submitted() {
|
||||
let (mut cx, state, mut visual) = input_window("draft", false);
|
||||
@@ -763,6 +731,36 @@ fn the_reveal_toggle_flips_masking() {
|
||||
assert!(!revealed, "the toggle should mask the field again");
|
||||
}
|
||||
|
||||
/// The reveal pill sits with equal insets on its three free sides; the
|
||||
/// small control metric must stay whole-pixel or snapping skews it.
|
||||
#[test]
|
||||
fn the_reveal_pill_insets_equally() {
|
||||
let (cx, _state, mut visual) = reveal_window();
|
||||
|
||||
let field = visual.debug_bounds("input-field").expect("field missing");
|
||||
let pill = visual
|
||||
.debug_bounds("input-reveal-button")
|
||||
.expect("reveal pill missing");
|
||||
|
||||
cx.update(|cx| {
|
||||
let theme = Theme::global(cx);
|
||||
let small = theme.sizes.resolve(Size::Small);
|
||||
let border = BORDER_WIDTH;
|
||||
let docked = small.control_height + border * 2.0;
|
||||
let inset = ((field.size.height - border * 2.0 - docked) / 2.0).max(px(0.));
|
||||
|
||||
let top_gap = pill.top() - (field.top() + border);
|
||||
let bottom_gap = (field.bottom() - border) - pill.bottom();
|
||||
let right_gap = (field.right() - border) - pill.right();
|
||||
assert!(
|
||||
(top_gap - inset).abs() < px(0.01)
|
||||
&& (bottom_gap - inset).abs() < px(0.01)
|
||||
&& (right_gap - inset).abs() < px(0.01),
|
||||
"pill gaps top {top_gap:?} bottom {bottom_gap:?} right {right_gap:?} vs inset {inset:?}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caret_can_rest_on_a_line_break() {
|
||||
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||
@@ -906,3 +904,131 @@ fn vertical_scroll_keeps_the_caret_in_view() {
|
||||
.unwrap_or_default();
|
||||
assert!(first_row_top < px(0.), "first row top: {first_row_top:?}");
|
||||
}
|
||||
|
||||
struct EdgeHarness {
|
||||
state: Entity<InputState>,
|
||||
clicked: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl Render for EdgeHarness {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let clicked = self.clicked.clone();
|
||||
div().w(px(400.)).p_4().child(
|
||||
Input::new("edge-input", self.state.clone())
|
||||
.placeholder("Search")
|
||||
.prefix(
|
||||
Button::new("edge-input-prefix-button")
|
||||
.icon(IconRole::Search)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small(),
|
||||
)
|
||||
.suffix(
|
||||
Button::new("edge-input-suffix-button")
|
||||
.debug_selector(|| "edge-suffix-button".to_string())
|
||||
.icon(IconRole::Plus)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small()
|
||||
.on_click(move |_, _, _| clicked.set(true)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn edge_input_window(
|
||||
initial: &str,
|
||||
) -> (
|
||||
TestAppContext,
|
||||
Entity<InputState>,
|
||||
VisualTestContext,
|
||||
Rc<Cell<bool>>,
|
||||
) {
|
||||
let clicked = Rc::new(Cell::new(false));
|
||||
let clicked_for_window = clicked.clone();
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
bind_input_keys(cx, InputBindings::default());
|
||||
});
|
||||
let state = cx.update(|cx| InputState::new_with_text(initial, cx));
|
||||
let state_for_window = state.clone();
|
||||
let window = cx.add_window(move |_, _| EdgeHarness {
|
||||
state: state_for_window,
|
||||
clicked: clicked_for_window,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
(cx, state, visual, clicked)
|
||||
}
|
||||
|
||||
/// Prefix and suffix dock flush inside the field, prefix leading, and a
|
||||
/// small button in a zone centers with equal insets like the reveal pill.
|
||||
#[test]
|
||||
fn prefix_and_suffix_dock_inside_the_field() {
|
||||
let (cx, _state, mut visual, _clicked) = edge_input_window("abc");
|
||||
|
||||
let field = visual.debug_bounds("input-field").expect("field missing");
|
||||
let prefix = visual
|
||||
.debug_bounds("input-prefix")
|
||||
.expect("prefix zone missing");
|
||||
let suffix = visual
|
||||
.debug_bounds("input-suffix")
|
||||
.expect("suffix zone missing");
|
||||
let pill = visual
|
||||
.debug_bounds("edge-suffix-button")
|
||||
.expect("suffix button missing");
|
||||
|
||||
assert!(prefix.left() >= field.left());
|
||||
assert!(prefix.right() <= suffix.left());
|
||||
assert!(suffix.right() <= field.right());
|
||||
|
||||
cx.update(|cx| {
|
||||
let theme = Theme::global(cx);
|
||||
let small = theme.sizes.resolve(Size::Small);
|
||||
let border = BORDER_WIDTH;
|
||||
let docked = small.control_height + border * 2.0;
|
||||
let inset = ((field.size.height - border * 2.0 - docked) / 2.0).max(px(0.));
|
||||
|
||||
let top_gap = pill.top() - (field.top() + border);
|
||||
let bottom_gap = (field.bottom() - border) - pill.bottom();
|
||||
let right_gap = (field.right() - border) - pill.right();
|
||||
assert!(
|
||||
(top_gap - inset).abs() < px(0.01)
|
||||
&& (bottom_gap - inset).abs() < px(0.01)
|
||||
&& (right_gap - inset).abs() < px(0.01),
|
||||
"suffix pill gaps top {top_gap:?} bottom {bottom_gap:?} right {right_gap:?} vs inset {inset:?}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// A click on a docked control activates it without running the field's
|
||||
/// caret handler underneath.
|
||||
#[test]
|
||||
fn a_suffix_click_activates_without_focusing_the_field() {
|
||||
let (cx, state, mut visual, clicked) = edge_input_window("abc");
|
||||
|
||||
let bounds = visual
|
||||
.debug_bounds("input-suffix")
|
||||
.expect("suffix zone missing");
|
||||
click(
|
||||
&mut visual,
|
||||
point(
|
||||
bounds.left() + bounds.size.width / 2.0,
|
||||
bounds.top() + bounds.size.height / 2.0,
|
||||
),
|
||||
);
|
||||
|
||||
assert!(clicked.get(), "the suffix button should activate");
|
||||
let focused = visual.update(|window, cx| state.read(cx).focus_handle().is_focused(window));
|
||||
assert!(!focused, "the suffix click must not focus the field");
|
||||
|
||||
// The zone must not swallow text interaction either.
|
||||
focus(&mut visual, &state);
|
||||
visual.simulate_keystrokes("x");
|
||||
assert_eq!(value(&cx, &state), "abcx");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user