mod common;
use gpui_kit::test::{TestAppContextExt, TestSupportExt, TestWindowExt};
use gpui_kit::{
AppContext, Context, MouseButton, ScrollDelta, ScrollHandle, TestAppContext, Window, div,
point, prelude::*, px, size,
};
use std::{cell::RefCell, rc::Rc, time::Duration};
struct Scopes {
clicks: Rc<RefCell<Vec<&'static str>>>,
}
impl Render for Scopes {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().flex().children(["toolbar", "dialog"].map(|scope| {
let clicks = self.clicks.clone();
div().id(scope).size(px(80.)).child(
div().id("footer").child(
div()
.id("save")
.test_support()
.aria_label(scope)
.size(px(40.))
.on_click(move |_, _, _| clicks.borrow_mut().push(scope)),
),
)
}))
}
}
#[gpui_kit::test]
fn scoped_queries_follow_existing_gpui_paths_without_observed_containers(cx: &mut TestAppContext) {
let clicks = Rc::new(RefCell::new(vec![]));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|_| Scopes {
clicks: clicks.clone(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert_eq!(
window.within("toolbar").find("save").label(),
Some("toolbar")
);
window.within("dialog").within("footer").click("save", cx);
assert!(window.within("toolbar").try_find("missing").is_none());
})
.unwrap();
assert_eq!(&*clicks.borrow(), &["dialog"]);
}
#[gpui_kit::test]
#[should_panic(expected = "Registered paths:")]
fn missing_targets_explain_the_registered_frame(cx: &mut TestAppContext) {
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|_| Scopes {
clicks: Default::default(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
window.find("misspelled");
})
.unwrap();
}
struct Pointer {
events: Rc<RefCell<Vec<String>>>,
}
impl Render for Pointer {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let hover = self.events.clone();
let left = self.events.clone();
let right = self.events.clone();
div()
.id("surface")
.test_support()
.size(px(80.))
.on_hover(move |entered, _, _| {
if *entered {
hover.borrow_mut().push("hover".into());
}
})
.on_mouse_up(MouseButton::Left, move |event, _, _| {
left.borrow_mut()
.push(format!("left:{}", event.click_count))
})
.on_mouse_up(MouseButton::Right, move |_, _, _| {
right.borrow_mut().push("right".into())
})
}
}
#[gpui_kit::test]
fn hover_right_click_and_double_click_dispatch_native_pointer_events(cx: &mut TestAppContext) {
let events = Rc::new(RefCell::new(vec![]));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|_| Pointer {
events: events.clone(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.hover("surface", cx);
window.right_click("surface", cx);
window.double_click("surface", cx);
})
.unwrap();
let events = events.borrow();
assert!(events.iter().any(|event| event == "hover"));
assert!(events.iter().any(|event| event == "right"));
assert!(events.windows(2).any(|pair| pair == ["left:1", "left:2"]));
}
struct Scrolling {
scroll: ScrollHandle,
}
impl Render for Scrolling {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.id("list")
.test_support()
.w(px(100.))
.h(px(60.))
.overflow_y_scroll()
.track_scroll(&self.scroll)
.flex()
.flex_col()
.children((0..30usize).map(|index| {
div()
.id(("row", index))
.test_support()
.h(px(20.))
.flex_shrink_0()
.child(format!("Row {index}"))
}))
}
}
#[gpui_kit::test]
fn scrolling_changes_clipping_and_resolved_row_positions(cx: &mut TestAppContext) {
let scroll = ScrollHandle::new();
let (handle, _) = common::open_window(cx, Some(size(px(200.), px(200.))), |_, cx| {
cx.new(|_| Scrolling {
scroll: scroll.clone(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert!(!window.find(("row", 10usize)).visible());
window.scroll("list", ScrollDelta::Pixels(point(px(0.), px(-200.))), cx);
assert!(window.find(("row", 10usize)).visible());
assert!(!window.find(("row", 0usize)).visible());
})
.unwrap();
assert!(scroll.offset().y < px(0.));
}
#[derive(Clone)]
struct Payload;
impl Render for Payload {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size(px(8.))
}
}
struct Dropping {
drops: Rc<RefCell<usize>>,
}
impl Render for Dropping {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let drops = self.drops.clone();
div()
.flex()
.gap_8()
.child(
div()
.id("source")
.test_support()
.size(px(40.))
.on_drag(Payload, |payload, _, _, cx| cx.new(|_| payload.clone())),
)
.child(
div()
.id("target")
.test_support()
.size(px(40.))
.on_drop(move |_: &Payload, _, _| *drops.borrow_mut() += 1),
)
}
}
#[gpui_kit::test]
fn dragging_runs_gpui_drag_creation_and_drop_hit_testing(cx: &mut TestAppContext) {
let drops = Rc::new(RefCell::new(0));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|_| Dropping {
drops: drops.clone(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
let from = window.find("source").bounds().center();
let to = window.find("target").bounds().center();
window.drag(from, to, cx);
})
.unwrap();
assert_eq!(*drops.borrow(), 1);
}
struct Loading {
ready: bool,
}
impl Render for Loading {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().when(self.ready, |this| {
this.child(div().id("loaded").test_support().size(px(20.)))
})
}
}
#[gpui_kit::test]
async fn wait_for_drives_test_time_and_refreshes_async_changes(cx: &mut TestAppContext) {
let (handle, handle_content) =
common::open_window(cx, None, |_, cx| cx.new(|_| Loading { ready: false }));
let executor = cx.executor();
cx.spawn(move |mut cx| async move {
executor.timer(Duration::from_millis(25)).await;
handle_content.update(&mut cx, |view, cx| {
view.ready = true;
cx.notify();
});
})
.detach();
cx.wait_for(handle.into(), Duration::from_millis(100), |window, _| {
window.try_find("loaded").is_some()
})
.await;
}
#[gpui_kit::test]
#[should_panic(expected = "UI condition timed out")]
async fn wait_for_has_a_bounded_failure(cx: &mut TestAppContext) {
let (handle, _) = common::open_window(cx, None, |_, cx| cx.new(|_| Loading { ready: false }));
cx.wait_for(handle.into(), Duration::from_millis(20), |window, _| {
window.try_find("loaded").is_some()
})
.await;
}
struct ChoiceStates {
checkbox: gpui_kit::base::CheckboxState,
radio: bool,
pressed: bool,
}
impl Render for ChoiceStates {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let checkbox = cx.entity();
let radio = cx.entity();
let toggle = cx.entity();
div()
.flex()
.gap_4()
.child(
gpui_kit::base::Checkbox::new("mixed")
.state(self.checkbox)
.size(px(40.))
.on_change(move |state, _, _, cx| {
checkbox.update(cx, |this, cx| {
this.checkbox = state;
cx.notify();
})
}),
)
.child(
gpui_kit::base::Radio::new("radio")
.checked(self.radio)
.size(px(40.))
.on_change(move |checked, _, _, cx| {
radio.update(cx, |this, cx| {
this.radio = checked;
cx.notify();
})
}),
)
.child(
gpui_kit::base::Toggle::new("toggle")
.pressed(self.pressed)
.size(px(40.))
.on_change(move |pressed, _, _, cx| {
toggle.update(cx, |this, cx| {
this.pressed = pressed;
cx.notify();
})
}),
)
}
}
#[gpui_kit::test]
fn mixed_checkbox_radio_and_toggle_report_distinct_states(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|_| ChoiceStates {
checkbox: gpui_kit::base::CheckboxState::Indeterminate,
radio: false,
pressed: false,
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert_eq!(window.find("mixed").checked(), Some(false));
assert_eq!(window.find("mixed").indeterminate(), Some(true));
window.click("mixed", cx);
assert_eq!(window.find("mixed").checked(), Some(true));
assert_eq!(window.find("mixed").indeterminate(), Some(false));
window.click("radio", cx);
assert_eq!(window.find("radio").checked(), Some(true));
assert_eq!(window.find("radio").selected(), Some(true));
window.click("toggle", cx);
assert_eq!(window.find("toggle").checked(), Some(true));
assert_eq!(
window.find("toggle").expanded(),
None,
"unsupported properties remain unknown"
);
})
.unwrap();
}
struct VirtualRows {
scroll: gpui_kit::base::VirtualListScrollHandle,
}
impl Render for VirtualRows {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("viewport")
.test_support()
.w(px(100.))
.h(px(60.))
.child(
gpui_kit::base::v_virtual_list(
cx.entity(),
"rows",
Rc::new(vec![size(px(100.), px(20.)); 1000]),
|_, range, _, _| {
range
.map(|index| {
div()
.id(("virtual-row", index))
.test_support()
.h(px(20.))
.child(format!("Row {index}"))
})
.collect()
},
)
.track_scroll(&self.scroll),
)
}
}
#[gpui_kit::test]
fn scrolling_a_real_virtual_list_registers_new_rows_and_releases_old_ones(cx: &mut TestAppContext) {
let (handle, _) = common::open_window(cx, Some(size(px(200.), px(200.))), |_, cx| {
cx.new(|_| VirtualRows {
scroll: gpui_kit::base::VirtualListScrollHandle::new(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert!(window.find(("virtual-row", 0usize)).visible());
assert!(window.try_find(("virtual-row", 50usize)).is_none());
window.scroll(
"viewport",
ScrollDelta::Pixels(point(px(0.), px(-1000.))),
cx,
);
assert!(window.find(("virtual-row", 50usize)).visible());
assert!(window.try_find(("virtual-row", 0usize)).is_none());
})
.unwrap();
}
struct Pair<T: Render> {
left: gpui_kit::Entity<T>,
right: gpui_kit::Entity<T>,
}
impl<T: Render> Render for Pair<T> {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.gap_4()
.p_4()
.child(div().id("left").w(px(180.)).child(self.left.clone()))
.child(div().id("right").w(px(180.)).child(self.right.clone()))
}
}
#[gpui_kit::test]
fn scoped_pointer_events_do_not_reach_duplicate_ids_in_another_scope(cx: &mut TestAppContext) {
let left = Rc::new(RefCell::new(vec![]));
let right = Rc::new(RefCell::new(vec![]));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|cx| Pair {
left: cx.new(|_| Pointer {
events: left.clone(),
}),
right: cx.new(|_| Pointer {
events: right.clone(),
}),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
let mut dialog = window.within("right");
dialog.hover("surface", cx);
dialog.right_click("surface", cx);
dialog.double_click("surface", cx);
})
.unwrap();
assert!(left.borrow().is_empty());
let events = right.borrow();
assert!(events.iter().any(|event| event == "hover"));
assert!(events.iter().any(|event| event == "right"));
assert!(events.windows(2).any(|pair| pair == ["left:1", "left:2"]));
}
#[gpui_kit::test]
fn scoped_scroll_moves_only_the_selected_list(cx: &mut TestAppContext) {
let left = ScrollHandle::new();
let right = ScrollHandle::new();
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|cx| Pair {
left: cx.new(|_| Scrolling {
scroll: left.clone(),
}),
right: cx.new(|_| Scrolling {
scroll: right.clone(),
}),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
window
.within("right")
.scroll("list", ScrollDelta::Pixels(point(px(0.), px(-200.))), cx);
assert!(window.within("right").find(("row", 10usize)).visible());
assert!(!window.within("left").find(("row", 10usize)).visible());
})
.unwrap();
assert_eq!(left.offset().y, px(0.));
assert!(right.offset().y < px(0.));
}
#[gpui_kit::test]
fn scoped_drag_to_resolves_both_ids_inside_the_scope(cx: &mut TestAppContext) {
let left = Rc::new(RefCell::new(0));
let right = Rc::new(RefCell::new(0));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|cx| Pair {
left: cx.new(|_| Dropping {
drops: left.clone(),
}),
right: cx.new(|_| Dropping {
drops: right.clone(),
}),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
window.within("right").drag_to("source", "target", cx);
})
.unwrap();
assert_eq!(*left.borrow(), 0);
assert_eq!(*right.borrow(), 1);
}
#[gpui_kit::test]
fn drag_to_uses_native_drop_dispatch(cx: &mut TestAppContext) {
let drops = Rc::new(RefCell::new(0));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|_| Dropping {
drops: drops.clone(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.drag_to("source", "target", cx);
})
.unwrap();
assert_eq!(*drops.borrow(), 1);
}
struct KeyboardJump {
left: gpui_kit::FocusHandle,
right: gpui_kit::FocusHandle,
keys: Rc<RefCell<Vec<String>>>,
}
impl Render for KeyboardJump {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let keys_left = self.keys.clone();
let keys_right = self.keys.clone();
let next = self.left.clone();
div()
.flex()
.child(
div().id("left").child(
div()
.id("field")
.test_support()
.track_focus(&self.left)
.size(px(40.))
.on_key_down(move |event, _, _| {
keys_left.borrow_mut().push(event.keystroke.key.clone())
}),
),
)
.child(
div().id("right").child(
div()
.id("field")
.test_support()
.track_focus(&self.right)
.size(px(40.))
.on_key_down(move |event, window, cx| {
keys_right.borrow_mut().push(event.keystroke.key.clone());
next.focus(window, cx);
}),
),
)
}
}
#[gpui_kit::test]
fn scoped_input_stops_when_a_handler_moves_focus_to_another_scope(cx: &mut TestAppContext) {
let keys = Rc::new(RefCell::new(vec![]));
let (handle, handle_content) = common::open_window(cx, None, |_, cx| {
cx.new(|cx| KeyboardJump {
left: cx.focus_handle(),
right: cx.focus_handle(),
keys: keys.clone(),
})
});
common::update_content(handle, &handle_content, cx, |view, window, cx| {
view.right.focus(window, cx)
})
.unwrap();
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
let error = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
window.within("right").input("ab", cx);
}))
.expect_err("the second character must not reach the other scope");
assert!(
error
.downcast_ref::<String>()
.unwrap()
.contains("no observed keyboard focus inside scope")
);
assert_eq!(window.within("left").find("field").focused(), Some(true));
})
.unwrap();
assert_eq!(&*keys.borrow(), &["a"]);
}
struct KeyboardFrames {
focus: gpui_kit::FocusHandle,
renders: Rc<std::cell::Cell<usize>>,
keys: Rc<RefCell<Vec<String>>>,
}
struct KeyboardEvents {
focus: gpui_kit::FocusHandle,
events: Rc<RefCell<Vec<(bool, gpui_kit::Keystroke)>>>,
clicks: Rc<std::cell::Cell<usize>>,
}
impl Render for KeyboardEvents {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let down = self.events.clone();
let up = self.events.clone();
let clicks = self.clicks.clone();
div()
.id("keyboard-scope")
.child(
div()
.id("keys")
.test_support()
.track_focus(&self.focus)
.size(px(40.))
.on_key_down(move |event, _, cx| {
down.borrow_mut().push((true, event.keystroke.clone()));
cx.stop_propagation();
})
.on_key_up(move |event, _, _| {
up.borrow_mut().push((false, event.keystroke.clone()));
}),
)
.child(
gpui_kit::base::Button::new("activate")
.size(px(40.))
.on_click(move |_, _, _| clicks.set(clicks.get() + 1)),
)
}
}
#[gpui_kit::test]
fn press_sends_key_down_and_up_without_synthetic_enter_or_tab_text(cx: &mut TestAppContext) {
let events = Rc::new(RefCell::new(Vec::new()));
let clicks = Rc::new(std::cell::Cell::new(0));
let (handle, _) = common::open_window(cx, None, |_, cx| {
cx.new(|cx| KeyboardEvents {
focus: cx.focus_handle(),
events: events.clone(),
clicks: clicks.clone(),
})
});
cx.update_window(handle.into(), |_, window, cx| {
window.click("keys", cx);
for scoped in [false, true] {
for key in ["enter", "shift-enter", "tab", "shift-tab"] {
events.borrow_mut().clear();
if scoped {
window.within("keyboard-scope").press(key, cx);
} else {
window.press(key, cx);
}
let events = events.borrow();
assert_eq!(events.len(), 2, "{key}, scoped={scoped}");
assert!(events[0].0);
assert!(!events[1].0);
assert_eq!(events[0].1, gpui_kit::Keystroke::parse(key).unwrap());
assert_eq!(events[0].1, events[1].1);
assert_eq!(events[0].1.key_char, None);
}
}
window.click("activate", cx);
clicks.set(0);
window.press("enter", cx);
assert_eq!(clicks.get(), 1);
window.within("keyboard-scope").press("space", cx);
assert_eq!(clicks.get(), 2);
})
.unwrap();
}
impl Render for KeyboardFrames {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
self.renders.set(self.renders.get() + 1);
let keys = self.keys.clone();
div().id("editor-scope").child(
div()
.id("editor")
.test_support()
.track_focus(&self.focus)
.size(px(40.))
.on_key_down(move |event, _, _| {
keys.borrow_mut().push(event.keystroke.key.clone());
}),
)
}
}
#[gpui_kit::test]
fn scoped_keyboard_refreshes_without_extra_renders(cx: &mut TestAppContext) {
let renders = Rc::new(std::cell::Cell::new(0));
let keys = Rc::new(RefCell::new(vec![]));
let (handle, handle_content) = common::open_window(cx, None, |_, cx| {
cx.new(|cx| KeyboardFrames {
focus: cx.focus_handle(),
renders: renders.clone(),
keys: keys.clone(),
})
});
let focus =
common::update_content(handle, &handle_content, cx, |view, _, _| view.focus.clone())
.unwrap();
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
window.focus(&focus, cx);
renders.set(0);
window.within("editor-scope").input("abcdef", cx);
assert_eq!(keys.borrow().len(), 6);
renders.set(0);
window.within("editor-scope").input("abcdef", cx);
let scoped_renders = renders.get();
renders.set(0);
window.input("abcdef", cx);
assert_eq!(renders.get(), scoped_renders);
assert_eq!(keys.borrow().len(), 18);
renders.set(0);
window.within("editor-scope").press("backspace", cx);
let scoped_renders = renders.get();
renders.set(0);
window.press("backspace", cx);
assert_eq!(renders.get(), scoped_renders);
assert_eq!(keys.borrow().len(), 20);
})
.unwrap();
}