228 lines
6.8 KiB
Rust
228 lines
6.8 KiB
Rust
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,
|
|
};
|
|
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
|
|
use gpui_native_core::init;
|
|
|
|
fn option(label: &'static str) -> ComboboxOption<SharedString> {
|
|
(SharedString::from(label), SharedString::from(label))
|
|
}
|
|
|
|
struct Host {
|
|
combobox: Entity<ComboboxState<SharedString>>,
|
|
}
|
|
|
|
impl Render for Host {
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
div().size_full().p_4().child(self.combobox.clone())
|
|
}
|
|
}
|
|
|
|
type Recorder = Rc<RefCell<Vec<Option<SharedString>>>>;
|
|
type CreatedRecorder = Rc<RefCell<Vec<SharedString>>>;
|
|
|
|
struct ComboHost {
|
|
cx: TestAppContext,
|
|
state: Entity<ComboboxState<SharedString>>,
|
|
changes: Recorder,
|
|
created: CreatedRecorder,
|
|
visual: VisualTestContext,
|
|
}
|
|
|
|
fn combo_host(keep_unmatched: bool, create: bool) -> ComboHost {
|
|
let cx = TestAppContext::single();
|
|
// `init` registers the input bindings and then the combobox's, in the
|
|
// load-bearing order.
|
|
cx.update(init);
|
|
let changes: Recorder = Rc::new(RefCell::new(Vec::new()));
|
|
let created: Rc<RefCell<Vec<SharedString>>> = Rc::new(RefCell::new(Vec::new()));
|
|
let state_cell: Rc<RefCell<Option<Entity<ComboboxState<SharedString>>>>> =
|
|
Rc::new(RefCell::new(None));
|
|
let window = cx.update(|cx| {
|
|
let changes_for_builder = changes.clone();
|
|
let created_for_builder = created.clone();
|
|
let cell = state_cell.clone();
|
|
cx.open_window(gpui::WindowOptions::default(), |window, cx| {
|
|
let mut builder = Combobox::new(
|
|
"test-combo",
|
|
"Fruit",
|
|
vec![option("main"), option("maintain"), option("release")],
|
|
)
|
|
.on_change(move |value, _, _| changes_for_builder.borrow_mut().push(value));
|
|
if keep_unmatched {
|
|
builder = builder.keep_unmatched_text();
|
|
}
|
|
if create {
|
|
builder = builder
|
|
.on_create(move |text, _, _| created_for_builder.borrow_mut().push(text));
|
|
}
|
|
let combobox = cx.new(|cx| ComboboxState::new(builder, window, cx));
|
|
cell.borrow_mut().replace(combobox.clone());
|
|
cx.new(|_| Host { combobox })
|
|
})
|
|
.unwrap()
|
|
});
|
|
let state = state_cell.borrow().clone().expect("state captured");
|
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
|
draw(&mut visual);
|
|
ComboHost {
|
|
cx,
|
|
state,
|
|
changes,
|
|
created,
|
|
visual,
|
|
}
|
|
}
|
|
|
|
fn draw(visual: &mut VisualTestContext) {
|
|
visual.update(|window, cx| {
|
|
window.draw(cx).clear(cx);
|
|
});
|
|
}
|
|
|
|
fn focus(host: &mut ComboHost) {
|
|
let state = host.state.clone();
|
|
host.visual.update(|window, cx| {
|
|
state.update(cx, |state, cx| state.focus(window, cx));
|
|
});
|
|
}
|
|
|
|
fn type_text(host: &mut ComboHost, text: &str) {
|
|
focus(host);
|
|
for letter in text.chars() {
|
|
host.visual.simulate_keystrokes(&letter.to_string());
|
|
}
|
|
draw(&mut host.visual);
|
|
}
|
|
|
|
#[test]
|
|
fn typing_opens_the_picker_and_filters_the_options() {
|
|
let mut host = combo_host(false, false);
|
|
type_text(&mut host, "mai");
|
|
|
|
assert!(host.visual.debug_bounds("test-combo-option-0").is_some());
|
|
assert!(host.visual.debug_bounds("test-combo-option-1").is_some());
|
|
assert!(
|
|
host.visual.debug_bounds("test-combo-option-2").is_none(),
|
|
"release must not match \"mai\""
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn enter_commits_the_highlighted_option() {
|
|
let mut host = combo_host(false, false);
|
|
type_text(&mut host, "mai");
|
|
|
|
host.visual.simulate_keystrokes("enter");
|
|
draw(&mut host.visual);
|
|
|
|
assert_eq!(
|
|
*host.changes.borrow(),
|
|
vec![Some(SharedString::from("main"))],
|
|
"the first surviving option starts highlighted"
|
|
);
|
|
assert!(host.visual.debug_bounds("test-combo-panel").is_none());
|
|
let text = host.cx.update(|cx| host.state.read(cx).text(cx));
|
|
assert_eq!(text, "main");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_clears_the_value() {
|
|
let mut host = combo_host(false, false);
|
|
type_text(&mut host, "main");
|
|
host.visual.simulate_keystrokes("enter");
|
|
draw(&mut host.visual);
|
|
assert_eq!(
|
|
*host.changes.borrow(),
|
|
vec![Some(SharedString::from("main"))]
|
|
);
|
|
|
|
// Typing again first clears the value it no longer names.
|
|
host.visual.simulate_keystrokes("r");
|
|
draw(&mut host.visual);
|
|
assert_eq!(
|
|
*host.changes.borrow(),
|
|
vec![Some(SharedString::from("main")), None]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn escape_closes_without_committing() {
|
|
let mut host = combo_host(false, false);
|
|
type_text(&mut host, "mai");
|
|
|
|
host.visual.simulate_keystrokes("escape");
|
|
draw(&mut host.visual);
|
|
|
|
assert!(host.visual.debug_bounds("test-combo-panel").is_none());
|
|
assert!(host.changes.borrow().is_empty(), "escape must not commit");
|
|
}
|
|
|
|
#[test]
|
|
fn blur_reverts_unmatched_text_to_the_empty_selection() {
|
|
let mut host = combo_host(false, false);
|
|
type_text(&mut host, "nonsense");
|
|
assert!(host.changes.borrow().is_empty());
|
|
|
|
host.visual.update(|window, cx| window.blur(cx));
|
|
draw(&mut host.visual);
|
|
|
|
assert!(host.changes.borrow().is_empty());
|
|
let text = host.cx.update(|cx| host.state.read(cx).text(cx));
|
|
assert_eq!(text, "", "revert with no value empties the field");
|
|
}
|
|
|
|
#[test]
|
|
fn keep_unmatched_leaves_the_text() {
|
|
let mut host = combo_host(true, false);
|
|
type_text(&mut host, "nonsense");
|
|
|
|
host.visual.update(|window, cx| window.blur(cx));
|
|
draw(&mut host.visual);
|
|
|
|
let text = host.cx.update(|cx| host.state.read(cx).text(cx));
|
|
assert_eq!(text, "nonsense", "keep leaves the text in the field");
|
|
// The picker started with no value, so typing cleared nothing and there
|
|
// was nothing to report.
|
|
assert!(host.changes.borrow().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn create_hands_unmatched_text_to_the_handler() {
|
|
let mut host = combo_host(false, true);
|
|
type_text(&mut host, "feature");
|
|
|
|
host.visual.update(|window, cx| window.blur(cx));
|
|
draw(&mut host.visual);
|
|
|
|
assert_eq!(
|
|
*host.created.borrow(),
|
|
vec![SharedString::from("feature")],
|
|
"create hands the text over on blur"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_a_row_commits_it() {
|
|
let mut host = combo_host(false, false);
|
|
type_text(&mut host, "rel");
|
|
|
|
let row = host
|
|
.visual
|
|
.debug_bounds("test-combo-option-0")
|
|
.expect("release row missing");
|
|
host.visual
|
|
.simulate_click(row.center(), gpui::Modifiers::default());
|
|
draw(&mut host.visual);
|
|
|
|
assert_eq!(
|
|
*host.changes.borrow(),
|
|
vec![Some(SharedString::from("release"))]
|
|
);
|
|
assert!(host.visual.debug_bounds("test-combo-panel").is_none());
|
|
}
|