use crate::harness;
use gpui::{point, prelude::*, px, Modifiers, MouseButton, TestAppContext, VisualTestContext};
use harness::{events, open_host, press, still, Events};
use herogpui_components::{MenuBar, MenuBarMenu, MenuItem};
const PANEL: &str = "menu-bar-menu";
fn host(cx: &mut TestAppContext) -> (Events, &mut VisualTestContext) {
still();
let seen = events();
let s = seen.clone();
let cx = open_host(cx, move || {
let (a, o) = (s.clone(), s.clone());
gpui::div()
.p(px(40.))
.child(
MenuBar::new(
"bar",
vec![
MenuBarMenu::new(
"file",
"File",
vec![MenuItem::new("new", "New"), MenuItem::new("open", "Open")],
),
MenuBarMenu::new(
"edit",
"Edit",
vec![MenuItem::new("undo", "Undo"), MenuItem::new("redo", "Redo")],
)
.disabled_keys(["redo"]),
MenuBarMenu::new("help", "Help", vec![MenuItem::new("about", "About")])
.is_disabled(true),
MenuBarMenu::new("view", "View", vec![MenuItem::new("zoom", "Zoom")]),
],
)
.on_action(move |menu, item, _, _| {
a.borrow_mut().push(format!("action:{menu}/{item}"));
})
.on_open_change(move |open, _, _| {
o.borrow_mut().push(format!(
"open:{}",
open.as_ref().map_or("none", |k| k.as_ref())
));
}),
)
.into_any_element()
});
(seen, cx)
}
fn frame(cx: &mut VisualTestContext) {
cx.executor()
.advance_clock(std::time::Duration::from_millis(300));
for _ in 0..3 {
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
}
fn trigger(cx: &mut VisualTestContext, key: &str) -> gpui::Bounds<gpui::Pixels> {
cx.debug_bounds(Box::leak(
format!("menu-bar-trigger-{key}").into_boxed_str(),
))
.expect("trigger laid out")
}
fn panel(cx: &mut VisualTestContext) -> Option<gpui::Bounds<gpui::Pixels>> {
cx.debug_bounds(PANEL)
}
fn click_trigger(cx: &mut VisualTestContext, key: &str) {
let at = trigger(cx, key).center();
cx.simulate_mouse_down(at, MouseButton::Left, Modifiers::none());
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
frame(cx);
}
fn log(seen: &Events) -> Vec<String> {
seen.borrow().clone()
}
#[gpui::test]
fn a_press_opens_below_the_trigger_and_again_closes(cx: &mut TestAppContext) {
let (seen, cx) = host(cx);
frame(cx);
assert!(panel(cx).is_none());
click_trigger(cx, "edit");
let edit = trigger(cx, "edit");
let bounds = panel(cx).expect("the Edit menu opened");
assert_eq!(
bounds.origin.x, edit.origin.x,
"start-aligned to its trigger"
);
assert!(bounds.origin.y >= edit.bottom(), "below its trigger");
click_trigger(cx, "edit");
assert!(panel(cx).is_none(), "pressing the open trigger closes it");
assert_eq!(log(&seen), ["open:edit", "open:none"]);
}
#[gpui::test]
fn arrows_rove_between_triggers_skipping_disabled(cx: &mut TestAppContext) {
let (seen, cx) = host(cx);
frame(cx);
press(cx, "tab");
frame(cx);
press(cx, "right");
press(cx, "right");
frame(cx);
press(cx, "enter");
frame(cx);
assert_eq!(log(&seen), ["open:view"], "Enter opened the focused View");
press(cx, "escape");
frame(cx);
assert!(panel(cx).is_none());
press(cx, "right");
press(cx, "down");
frame(cx);
press(cx, "escape");
frame(cx);
press(cx, "home");
press(cx, "left");
press(cx, "space");
frame(cx);
assert_eq!(
log(&seen),
[
"open:view",
"open:none",
"open:file",
"open:none",
"open:view"
]
);
}
#[gpui::test]
fn left_and_right_switch_the_open_menu(cx: &mut TestAppContext) {
let (seen, cx) = host(cx);
frame(cx);
press(cx, "tab");
press(cx, "down");
frame(cx);
assert!(panel(cx).is_some());
press(cx, "right");
frame(cx);
let at_edit = panel(cx).expect("switched, still open");
assert_eq!(at_edit.origin.x, trigger(cx, "edit").origin.x);
press(cx, "enter");
frame(cx);
assert!(panel(cx).is_none(), "choosing closes the bar");
assert_eq!(
log(&seen),
["open:file", "open:edit", "action:edit/undo", "open:none"]
);
press(cx, "left");
press(cx, "enter");
frame(cx);
assert_eq!(log(&seen).last().unwrap(), "open:file");
}
#[gpui::test]
fn hovering_another_trigger_switches_only_while_open(cx: &mut TestAppContext) {
let (seen, cx) = host(cx);
frame(cx);
let view = trigger(cx, "view").center();
cx.simulate_mouse_move(view, None, Modifiers::none());
frame(cx);
assert!(panel(cx).is_none(), "hover alone opens nothing");
click_trigger(cx, "file");
let help = trigger(cx, "help").center();
cx.simulate_mouse_move(help, None, Modifiers::none());
frame(cx);
let edit = trigger(cx, "edit").center();
cx.simulate_mouse_move(edit, None, Modifiers::none());
frame(cx);
assert_eq!(
panel(cx).expect("still open").origin.x,
trigger(cx, "edit").origin.x
);
assert_eq!(log(&seen), ["open:file", "open:edit"], "Help is disabled");
}
#[gpui::test]
fn escape_and_an_outside_press_close(cx: &mut TestAppContext) {
let (seen, cx) = host(cx);
frame(cx);
click_trigger(cx, "file");
assert!(panel(cx).is_some());
press(cx, "escape");
frame(cx);
assert!(panel(cx).is_none());
click_trigger(cx, "file");
cx.simulate_click(point(px(700.), px(500.)), Modifiers::none());
frame(cx);
assert!(panel(cx).is_none());
click_trigger(cx, "help");
assert!(panel(cx).is_none(), "a disabled item never opens");
assert_eq!(
log(&seen),
["open:file", "open:none", "open:file", "open:none"]
);
}
#[gpui::test]
fn an_open_menu_that_becomes_disabled_closes_the_bar(cx: &mut TestAppContext) {
still();
let seen = events();
let s = seen.clone();
let edit_disabled = std::rc::Rc::new(std::cell::Cell::new(false));
let d = edit_disabled.clone();
let cx = open_host(cx, move || {
let o = s.clone();
gpui::div()
.p(px(40.))
.child(
MenuBar::new(
"bar",
vec![
MenuBarMenu::new("file", "File", vec![MenuItem::new("new", "New")]),
MenuBarMenu::new("edit", "Edit", vec![MenuItem::new("undo", "Undo")])
.is_disabled(d.get()),
MenuBarMenu::new("view", "View", vec![MenuItem::new("zoom", "Zoom")]),
],
)
.on_open_change(move |open, _, _| {
o.borrow_mut().push(format!(
"open:{}",
open.as_ref().map_or("none", |k| k.as_ref())
));
}),
)
.into_any_element()
});
frame(cx);
click_trigger(cx, "edit");
assert!(panel(cx).is_some());
edit_disabled.set(true);
frame(cx);
frame(cx);
assert!(panel(cx).is_none());
assert_eq!(log(&seen), ["open:edit", "open:none"]);
let view = trigger(cx, "view").center();
cx.simulate_mouse_move(view, None, Modifiers::none());
frame(cx);
assert!(panel(cx).is_none(), "hover does not reopen a closed bar");
assert_eq!(log(&seen), ["open:edit", "open:none"]);
}