Files
gpui-native-kit/crates/core/tests/input.rs
T

366 lines
12 KiB
Rust

use gpui::{
Entity, Modifiers, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext,
Window, div, point, px,
};
use gpui_native_core::input::{Input, InputState, InputStateEvent};
use gpui_native_theme::Theme;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
struct Harness {
state: Entity<InputState>,
disabled: bool,
}
impl Render for Harness {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
div().size_full().child(
Input::new("test-input", self.state.clone())
.placeholder("Placeholder")
.disabled(self.disabled),
)
}
}
fn input_window(
initial: &str,
disabled: bool,
) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
let mut cx = TestAppContext::single();
cx.update(Theme::init);
let state = cx.update(|cx| InputState::new_with_text(initial, cx));
let state_for_window = state.clone();
let window = cx.add_window(move |_, _| Harness {
state: state_for_window,
disabled,
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
(cx, state, visual)
}
fn value(cx: &TestAppContext, state: &Entity<InputState>) -> String {
cx.update(|cx| state.read(cx).value().to_string())
}
fn focus(visual: &mut VisualTestContext, state: &Entity<InputState>) {
// Programmatic focus: a click would move the caret to the click point.
visual.update(|window, cx| {
let handle = state.read(cx).focus_handle();
window.focus(&handle, cx);
});
}
#[test]
fn typing_inserts_at_the_caret() {
let (cx, state, mut visual) = input_window("", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("h e l l o");
assert_eq!(value(&cx, &state), "hello");
visual.simulate_keystrokes("space w o r l d");
assert_eq!(value(&cx, &state), "hello world");
}
#[test]
fn arrow_left_moves_the_caret_and_typing_splits_the_text() {
let (cx, state, mut visual) = input_window("ab", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("left");
visual.simulate_keystrokes("x");
assert_eq!(value(&cx, &state), "axb");
}
#[test]
fn backspace_deletes_before_the_caret_and_delete_after() {
let (cx, state, mut visual) = input_window("abc", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("backspace");
assert_eq!(value(&cx, &state), "ab");
// The caret sits between 'a' and 'b'; forward delete removes 'b'.
visual.simulate_keystrokes("left");
visual.simulate_keystrokes("delete");
assert_eq!(value(&cx, &state), "a");
}
#[test]
fn alt_arrow_moves_by_words() {
let (cx, state, mut visual) = input_window("hello world", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("alt-left");
visual.simulate_keystrokes("x");
assert_eq!(value(&cx, &state), "hello xworld");
visual.simulate_keystrokes("alt-left alt-left");
visual.simulate_keystrokes("shift-right shift-right");
visual.simulate_keystrokes("z");
assert_eq!(value(&cx, &state), "zllo xworld");
}
#[test]
fn select_all_then_type_replaces_the_value() {
let (cx, state, mut visual) = input_window("abcd", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("cmd-a");
visual.simulate_keystrokes("z");
assert_eq!(value(&cx, &state), "z");
}
#[cfg(target_os = "macos")]
#[test]
fn cmd_arrow_jumps_to_the_field_ends() {
let (cx, state, mut visual) = input_window("hello world", false);
focus(&mut visual, &state);
// Caret starts at the end; cmd-left brings it to the start.
visual.simulate_keystrokes("cmd-left");
visual.simulate_keystrokes("x");
assert_eq!(value(&cx, &state), "xhello world");
// cmd-shift-right selects from the caret to the end.
visual.simulate_keystrokes("cmd-shift-right");
visual.simulate_keystrokes("backspace");
assert_eq!(value(&cx, &state), "x");
// cmd-up / cmd-down mirror the jumps.
visual.simulate_keystrokes("cmd-down");
visual.simulate_keystrokes("x");
assert_eq!(value(&cx, &state), "xx");
visual.simulate_keystrokes("cmd-up");
visual.simulate_keystrokes("y");
assert_eq!(value(&cx, &state), "yxx");
}
#[cfg(target_os = "macos")]
#[test]
fn cmd_backspace_deletes_to_the_field_start() {
let (cx, state, mut visual) = input_window("hello", false);
focus(&mut visual, &state);
// The caret starts at the end; cmd-backspace clears the whole field.
visual.simulate_keystrokes("cmd-backspace");
assert_eq!(value(&cx, &state), "");
}
#[test]
fn undo_reverts_a_typing_burst_as_one_step() {
let (cx, state, mut visual) = input_window("", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("h e l l o");
visual.simulate_keystrokes("cmd-z");
assert_eq!(value(&cx, &state), "");
visual.simulate_keystrokes("cmd-shift-z");
assert_eq!(value(&cx, &state), "hello");
}
#[test]
fn undo_steps_through_edits_outside_the_group_interval() {
let (cx, state, mut visual) = input_window("", false);
focus(&mut visual, &state);
visual.update(|_, cx| {
state.update(cx, |state, _| state.set_group_interval(Duration::ZERO));
});
visual.simulate_keystrokes("a b");
assert_eq!(value(&cx, &state), "ab");
visual.simulate_keystrokes("cmd-z");
assert_eq!(value(&cx, &state), "a");
visual.simulate_keystrokes("cmd-z");
assert_eq!(value(&cx, &state), "");
visual.simulate_keystrokes("cmd-shift-z");
assert_eq!(value(&cx, &state), "a");
visual.simulate_keystrokes("cmd-shift-z");
assert_eq!(value(&cx, &state), "ab");
// A deletion gets its own entry and undoes back to the typed text.
visual.simulate_keystrokes("backspace");
assert_eq!(value(&cx, &state), "a");
visual.simulate_keystrokes("cmd-z");
assert_eq!(value(&cx, &state), "ab");
}
#[test]
fn new_edits_clear_the_redo_stack() {
let (cx, state, mut visual) = input_window("", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("a");
visual.simulate_keystrokes("cmd-z");
visual.simulate_keystrokes("b");
visual.simulate_keystrokes("cmd-shift-z");
assert_eq!(value(&cx, &state), "b");
visual.simulate_keystrokes("cmd-z");
assert_eq!(value(&cx, &state), "");
}
#[test]
fn set_value_clears_the_history() {
let (cx, state, mut visual) = input_window("", false);
focus(&mut visual, &state);
visual.simulate_keystrokes("typed");
visual.update(|window, cx| {
state.update(cx, |state, cx| state.set_value("preset", window, cx));
});
visual.simulate_keystrokes("cmd-z");
assert_eq!(value(&cx, &state), "preset");
}
#[test]
fn shift_arrow_selects_and_backspace_removes_the_selection() {
let (cx, state, mut visual) = input_window("ab", false);
focus(&mut visual, &state);
// Caret starts at the end; selecting left twice covers "ab".
visual.simulate_keystrokes("shift-left shift-left");
visual.simulate_keystrokes("backspace");
assert_eq!(value(&cx, &state), "");
}
#[test]
fn disabled_input_ignores_typing_and_clicks() {
// No programmatic focus: a disabled field must not take focus, so the
// key events fall to nothing and the input handler is never registered.
let (cx, state, mut visual) = input_window("", true);
visual.simulate_click(point(px(20.), px(14.)), Modifiers::default());
visual.simulate_keystrokes("h");
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.
fn record_events(
cx: &mut TestAppContext,
state: &Entity<InputState>,
) -> (Rc<RefCell<Vec<InputStateEvent>>>, gpui::Subscription) {
let sink = Rc::new(RefCell::new(Vec::new()));
let sink_for_sub = sink.clone();
let subscription = cx.update(|cx| {
cx.subscribe(state, move |_, event, _| {
sink_for_sub.borrow_mut().push(event.clone());
})
});
// Subscriptions activate through a deferred effect.
cx.run_until_parked();
(sink, subscription)
}
fn count(sink: &Rc<RefCell<Vec<InputStateEvent>>>, event: &InputStateEvent) -> usize {
sink.borrow().iter().filter(|e| e == &event).count()
}
#[test]
fn text_changed_emits_for_edits_but_not_for_caret_moves() {
let (mut cx, state, mut visual) = input_window("", false);
let (sink, _subscription) = record_events(&mut cx, &state);
focus(&mut visual, &state);
visual.simulate_keystrokes("a space b");
assert_eq!(count(&sink, &InputStateEvent::TextChanged), 3);
// Caret movement repaints but is not a text change.
visual.simulate_keystrokes("left");
assert_eq!(count(&sink, &InputStateEvent::TextChanged), 3);
}
#[test]
fn set_value_no_op_does_not_emit_changed() {
let (mut cx, state, mut visual) = input_window("same", false);
let (sink, _subscription) = record_events(&mut cx, &state);
visual.update(|window, cx| {
state.update(cx, |state, cx| state.set_value("same", window, cx));
});
visual.run_until_parked();
assert_eq!(count(&sink, &InputStateEvent::TextChanged), 0);
visual.update(|window, cx| {
state.update(cx, |state, cx| state.set_value("other", window, cx));
});
visual.run_until_parked();
assert_eq!(count(&sink, &InputStateEvent::TextChanged), 1);
}
#[test]
fn set_value_silent_updates_without_emitting() {
let (mut cx, state, mut visual) = input_window("same", false);
let (sink, _subscription) = record_events(&mut cx, &state);
visual.update(|window, cx| {
state.update(cx, |state, cx| state.set_value_silent("other", window, cx));
});
visual.run_until_parked();
assert_eq!(value(&cx, &state), "other");
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);
focus(&mut visual, &state);
cx.update(|cx| {
cx.write_to_clipboard(gpui::ClipboardItem::new_string("a\nb\rc".to_string()));
});
visual.simulate_keystrokes("cmd-v");
// `\r` is dropped and `\n` becomes a space, matching the sanitizer.
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);
let (sink, _subscription) = record_events(&mut cx, &state);
focus(&mut visual, &state);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
visual.simulate_keystrokes("enter");
assert_eq!(count(&sink, &InputStateEvent::Submitted), 1);
// The content is left in place for the subscriber to read or clear.
assert_eq!(value(&cx, &state), "draft");
}