736 lines
24 KiB
Rust
736 lines
24 KiB
Rust
use gpui::{
|
|
Entity, EntityInputHandler as _, KeyBinding, Modifiers, MouseButton, MouseDownEvent,
|
|
MouseUpEvent, ParentElement as _, Render, ScrollDelta, ScrollEvent, Styled as _,
|
|
TestAppContext, VisualTestContext, Window, div, point, px,
|
|
};
|
|
use gpui_native_core::input::{
|
|
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, bind_input_keys,
|
|
};
|
|
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) {
|
|
input_window_with_bindings(initial, disabled, InputBindings::default())
|
|
}
|
|
|
|
fn input_window_with_bindings(
|
|
initial: &str,
|
|
disabled: bool,
|
|
bindings: InputBindings,
|
|
) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
|
|
let mut cx = TestAppContext::single();
|
|
cx.update(|cx| {
|
|
Theme::init(cx);
|
|
bind_input_keys(cx, bindings);
|
|
});
|
|
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 read_only_refuses_edits_but_keeps_selection_and_copy() {
|
|
let (cx, state, mut visual) = input_window("hello", false);
|
|
focus(&mut visual, &state);
|
|
visual.update(|_, cx| {
|
|
state.update(cx, |state, cx| state.set_read_only(true, cx));
|
|
});
|
|
|
|
// Typing and backspace are refused.
|
|
visual.simulate_keystrokes("x backspace");
|
|
assert_eq!(value(&cx, &state), "hello");
|
|
|
|
// Paste is refused.
|
|
cx.update(|cx| {
|
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string("Z".to_string()));
|
|
});
|
|
visual.simulate_keystrokes("cmd-v");
|
|
assert_eq!(value(&cx, &state), "hello");
|
|
|
|
// Selection and copy still work.
|
|
visual.simulate_keystrokes("shift-left shift-left");
|
|
visual.simulate_keystrokes("cmd-c");
|
|
let clipboard = cx.update(|cx| {
|
|
cx.read_from_clipboard()
|
|
.and_then(|item| item.text())
|
|
.unwrap_or_default()
|
|
});
|
|
assert_eq!(clipboard, "lo");
|
|
|
|
// Cut neither changes the field nor touches the clipboard.
|
|
visual.simulate_keystrokes("cmd-x");
|
|
assert_eq!(value(&cx, &state), "hello");
|
|
let clipboard = cx.update(|cx| {
|
|
cx.read_from_clipboard()
|
|
.and_then(|item| item.text())
|
|
.unwrap_or_default()
|
|
});
|
|
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)));
|
|
assert!(!caret);
|
|
}
|
|
|
|
#[test]
|
|
fn caret_blinks_while_focused_and_holds_solid_while_typing() {
|
|
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, 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();
|
|
assert!(caret());
|
|
|
|
// Typing holds the caret solid and restarts the cycle.
|
|
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);
|
|
}
|
|
|
|
#[test]
|
|
fn horizontal_scroll_follows_the_caret_and_wheels_back() {
|
|
let text = "a".repeat(300);
|
|
let (_cx, state, mut visual) = input_window(&text, false);
|
|
focus(&mut visual, &state);
|
|
let draw = |visual: &mut VisualTestContext| {
|
|
visual.update(|window, cx| {
|
|
window.draw(cx).clear(cx);
|
|
});
|
|
};
|
|
|
|
// The text index at the field's left edge, through the last paint.
|
|
let left_edge_index = |visual: &mut VisualTestContext| -> usize {
|
|
visual
|
|
.update(|window, cx| {
|
|
state.update(cx, |state, cx| {
|
|
state.character_index_for_point(point(px(2.), px(14.)), window, cx)
|
|
})
|
|
})
|
|
.unwrap_or(0)
|
|
};
|
|
|
|
// The caret starts at the end; the view scrolls to keep it in view, so
|
|
// the left edge shows a later character.
|
|
draw(&mut visual);
|
|
let scrolled_index = left_edge_index(&mut visual);
|
|
assert!(scrolled_index > 0, "left edge index: {scrolled_index}");
|
|
|
|
// Moving to the start scrolls the view back: the left edge is index 0.
|
|
visual.simulate_keystrokes("home");
|
|
draw(&mut visual);
|
|
assert_eq!(left_edge_index(&mut visual), 0);
|
|
|
|
// A wheel scroll pans the view right without moving the caret.
|
|
visual.simulate_event(ScrollEvent {
|
|
position: point(px(200.), px(20.)),
|
|
delta: ScrollDelta::Pixels(point(px(-20.), px(0.))),
|
|
..Default::default()
|
|
});
|
|
draw(&mut visual);
|
|
let panned_index = left_edge_index(&mut visual);
|
|
assert!(panned_index > 0, "left edge index: {panned_index}");
|
|
}
|
|
|
|
#[test]
|
|
fn read_only_blocks_undo_redo_but_set_value_still_writes() {
|
|
let (cx, state, mut visual) = input_window("", false);
|
|
focus(&mut visual, &state);
|
|
|
|
visual.simulate_keystrokes("a");
|
|
visual.simulate_keystrokes("cmd-z");
|
|
assert_eq!(value(&cx, &state), "");
|
|
|
|
visual.update(|_, cx| {
|
|
state.update(cx, |state, cx| state.set_read_only(true, cx));
|
|
});
|
|
visual.simulate_keystrokes("cmd-shift-z");
|
|
assert_eq!(value(&cx, &state), "");
|
|
|
|
visual.update(|window, cx| {
|
|
state.update(cx, |state, cx| state.set_value("preset", window, cx));
|
|
});
|
|
assert_eq!(value(&cx, &state), "preset");
|
|
}
|
|
|
|
#[test]
|
|
fn escape_blurs_the_field() {
|
|
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.simulate_keystrokes("escape");
|
|
let focused = visual.update(|window, cx| state.read(cx).focus_handle().is_focused(window));
|
|
assert!(!focused);
|
|
visual.update(|window, cx| {
|
|
window.draw(cx).clear(cx);
|
|
});
|
|
visual.run_until_parked();
|
|
assert_eq!(count(&sink, &InputStateEvent::Blurred), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn custom_bindings_replace_and_unbind_keys() {
|
|
// Only submit is bound, and on a custom key.
|
|
let mut bindings = InputBindings::empty();
|
|
bindings.submit = Some(KeyBinding::new("cmd-enter", Submit, Some(INPUT_CONTEXT)));
|
|
let (mut cx, state, mut visual) = input_window_with_bindings("", false, bindings);
|
|
let (sink, _subscription) = record_events(&mut cx, &state);
|
|
focus(&mut visual, &state);
|
|
|
|
// The default enter binding is gone.
|
|
visual.simulate_keystrokes("enter");
|
|
assert_eq!(count(&sink, &InputStateEvent::Submitted), 0);
|
|
|
|
// The override fires.
|
|
visual.simulate_keystrokes("cmd-enter");
|
|
assert_eq!(count(&sink, &InputStateEvent::Submitted), 1);
|
|
|
|
// Unbound actions do nothing; unbound characters still type.
|
|
visual.simulate_keystrokes("cmd-z");
|
|
assert_eq!(value(&cx, &state), "");
|
|
visual.simulate_keystrokes("a");
|
|
assert_eq!(value(&cx, &state), "a");
|
|
}
|
|
|
|
#[test]
|
|
fn triple_click_selects_all_and_typing_replaces_it() {
|
|
let (cx, state, mut visual) = input_window("hello world", false);
|
|
focus(&mut visual, &state);
|
|
|
|
let at = point(px(30.), px(14.));
|
|
visual.simulate_event(MouseDownEvent {
|
|
position: at,
|
|
modifiers: Modifiers::default(),
|
|
button: MouseButton::Left,
|
|
click_count: 3,
|
|
first_mouse: false,
|
|
});
|
|
visual.simulate_event(MouseUpEvent {
|
|
position: at,
|
|
modifiers: Modifiers::default(),
|
|
button: MouseButton::Left,
|
|
click_count: 3,
|
|
});
|
|
|
|
// The selection spans the whole value: one keystroke replaces all of it.
|
|
visual.simulate_keystrokes("x");
|
|
assert_eq!(value(&cx, &state), "x");
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
|
|
fn password_window(initial: &str) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
|
|
let (cx, state, visual) = input_window(initial, false);
|
|
cx.update(|cx| {
|
|
state.update(cx, |state, cx| state.set_password(true, cx));
|
|
});
|
|
(cx, state, visual)
|
|
}
|
|
|
|
#[test]
|
|
fn password_fields_keep_the_real_buffer_while_masked() {
|
|
let (cx, state, mut visual) = password_window("");
|
|
focus(&mut visual, &state);
|
|
visual.simulate_keystrokes("s e c r e t");
|
|
// The buffer holds the real secret; masking is paint-only.
|
|
assert_eq!(value(&cx, &state), "secret");
|
|
|
|
// Editing still works through the mask.
|
|
visual.simulate_keystrokes("backspace");
|
|
assert_eq!(value(&cx, &state), "secre");
|
|
}
|
|
|
|
#[test]
|
|
fn password_fields_refuse_copy_and_cut() {
|
|
let (cx, state, mut visual) = password_window("secret");
|
|
focus(&mut visual, &state);
|
|
|
|
// Select everything and try to copy.
|
|
visual.simulate_keystrokes("cmd-a");
|
|
visual.simulate_keystrokes("cmd-c");
|
|
let clipboard = cx.update(|cx| {
|
|
cx.read_from_clipboard()
|
|
.and_then(|item| item.text())
|
|
.unwrap_or_default()
|
|
});
|
|
assert_eq!(clipboard, "");
|
|
|
|
// Cut neither leaks the secret nor deletes it.
|
|
visual.simulate_keystrokes("cmd-x");
|
|
assert_eq!(value(&cx, &state), "secret");
|
|
let clipboard = cx.update(|cx| {
|
|
cx.read_from_clipboard()
|
|
.and_then(|item| item.text())
|
|
.unwrap_or_default()
|
|
});
|
|
assert_eq!(clipboard, "");
|
|
}
|
|
|
|
#[test]
|
|
fn password_fields_still_allow_paste() {
|
|
let (cx, state, mut visual) = password_window("");
|
|
focus(&mut visual, &state);
|
|
cx.update(|cx| {
|
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string("hunter2".to_string()));
|
|
});
|
|
visual.simulate_keystrokes("cmd-v");
|
|
assert_eq!(value(&cx, &state), "hunter2");
|
|
}
|
|
|
|
#[test]
|
|
fn clicks_in_password_fields_map_to_real_offsets() {
|
|
let (cx, state, mut visual) = password_window("abcdefgh");
|
|
focus(&mut visual, &state);
|
|
|
|
// Clicking at the text origin moves the caret to the start, so typing
|
|
// lands there: the masked display maps clicks back onto real offsets.
|
|
click(&mut visual, point(px(12.), px(15.)));
|
|
visual.simulate_keystrokes("x");
|
|
assert_eq!(value(&cx, &state), "xabcdefgh");
|
|
|
|
// Clicking past the last bullet clamps to the end.
|
|
click(&mut visual, point(px(500.), px(15.)));
|
|
visual.simulate_keystrokes("y");
|
|
assert_eq!(value(&cx, &state), "xabcdefghy");
|
|
}
|
|
|
|
fn click(visual: &mut VisualTestContext, position: gpui::Point<gpui::Pixels>) {
|
|
visual.simulate_mouse_down(position, MouseButton::Left, Modifiers::default());
|
|
visual.simulate_mouse_up(position, MouseButton::Left, gpui::Modifiers::default());
|
|
}
|
|
|
|
struct RevealHarness {
|
|
state: Entity<InputState>,
|
|
}
|
|
|
|
impl Render for RevealHarness {
|
|
fn render(
|
|
&mut self,
|
|
_window: &mut Window,
|
|
_cx: &mut gpui::Context<Self>,
|
|
) -> impl gpui::IntoElement {
|
|
div()
|
|
.w(px(400.))
|
|
.p_4()
|
|
.child(Input::new("reveal-input", self.state.clone()).placeholder("Password"))
|
|
}
|
|
}
|
|
|
|
fn reveal_window() -> (TestAppContext, Entity<InputState>, VisualTestContext) {
|
|
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("", cx));
|
|
cx.update(|cx| {
|
|
state.update(cx, |state, cx| state.set_password(true, cx));
|
|
});
|
|
let state_for_window = state.clone();
|
|
let window = cx.add_window(move |_, _| RevealHarness {
|
|
state: state_for_window,
|
|
});
|
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
|
visual.update(|window, cx| {
|
|
window.draw(cx).clear(cx);
|
|
});
|
|
(cx, state, visual)
|
|
}
|
|
|
|
/// Clicking the reveal toggle flips masking on and off, so apps can reveal
|
|
/// a secret without a custom control.
|
|
#[test]
|
|
fn the_reveal_toggle_flips_masking() {
|
|
let (cx, state, mut visual) = reveal_window();
|
|
|
|
let bounds = visual
|
|
.debug_bounds("input-reveal")
|
|
.expect("reveal toggle missing");
|
|
let center = point(
|
|
bounds.left() + bounds.size.width / 2.0,
|
|
bounds.top() + bounds.size.height / 2.0,
|
|
);
|
|
|
|
// Masked: clicking reveals.
|
|
click(&mut visual, center);
|
|
let revealed = cx.update(|cx| state.read(cx).is_revealed());
|
|
assert!(revealed, "the toggle should reveal the field");
|
|
|
|
// Revealed: clicking masks again.
|
|
click(&mut visual, center);
|
|
let revealed = cx.update(|cx| state.read(cx).is_revealed());
|
|
assert!(!revealed, "the toggle should mask the field again");
|
|
}
|