use super::*;
#[test]
fn mouse_flags_default_all_enabled() {
let flags = MouseFlags::default();
assert!(flags.normal, "normal should be enabled by default");
assert!(flags.visual, "visual should be enabled by default");
assert!(flags.insert, "insert should be enabled by default");
assert!(flags.command, "command should be enabled by default");
}
#[test]
fn mouse_flags_set_to_n_only_normal_active() {
let flags = MouseFlags::from_flags("n");
assert!(flags.normal, "n flag enables normal");
assert!(!flags.visual, "only n: visual must be off");
assert!(!flags.insert, "only n: insert must be off");
assert!(!flags.command, "only n: command must be off");
}
#[test]
fn mouse_flags_set_empty_disables_all() {
let flags_empty = MouseFlags::from_flags("");
assert!(!flags_empty.normal, "empty string must disable normal");
assert!(!flags_empty.visual, "empty string must disable visual");
assert!(!flags_empty.insert, "empty string must disable insert");
assert!(!flags_empty.command, "empty string must disable command");
let flags_none = MouseFlags::none();
assert!(!flags_none.normal, "MouseFlags::none() must disable normal");
assert!(!flags_none.visual, "MouseFlags::none() must disable visual");
assert!(!flags_none.insert, "MouseFlags::none() must disable insert");
assert!(
!flags_none.command,
"MouseFlags::none() must disable command"
);
}
#[test]
fn mouse_flags_a_is_all_enabled() {
let flags = MouseFlags::from_flags("a");
assert!(flags.normal && flags.visual && flags.insert && flags.command);
}
#[test]
fn mouse_flags_nvi_multi_char() {
let flags = MouseFlags::from_flags("nvi");
assert!(flags.normal);
assert!(flags.visual);
assert!(flags.insert);
assert!(!flags.command);
}
#[test]
fn mouse_enabled_for_normal_mode_flags() {
let all = MouseFlags::all();
assert!(mouse_enabled_for(VimMode::Normal, &all));
let mut none_normal = MouseFlags::all();
none_normal.normal = false;
assert!(!mouse_enabled_for(VimMode::Normal, &none_normal));
}
#[test]
fn mouse_enabled_for_visual_mode_flags() {
let all = MouseFlags::all();
assert!(mouse_enabled_for(VimMode::Visual, &all));
assert!(mouse_enabled_for(VimMode::VisualLine, &all));
assert!(mouse_enabled_for(VimMode::VisualBlock, &all));
let mut no_visual = MouseFlags::all();
no_visual.visual = false;
assert!(!mouse_enabled_for(VimMode::Visual, &no_visual));
assert!(!mouse_enabled_for(VimMode::VisualLine, &no_visual));
assert!(!mouse_enabled_for(VimMode::VisualBlock, &no_visual));
}
#[test]
fn mouse_enabled_for_insert_mode_flags() {
let all = MouseFlags::all();
assert!(mouse_enabled_for(VimMode::Insert, &all));
let mut no_insert = MouseFlags::all();
no_insert.insert = false;
assert!(!mouse_enabled_for(VimMode::Insert, &no_insert));
}
#[test]
fn set_mouse_eq_flags_via_dispatch_ex() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(app.mouse_flags.normal && app.mouse_flags.visual && app.mouse_flags.insert);
app.dispatch_ex("set mouse=n");
assert!(app.mouse_flags.normal, "n: normal on");
assert!(!app.mouse_flags.visual, "n: visual off");
assert!(!app.mouse_flags.insert, "n: insert off");
app.dispatch_ex("set nomouse");
assert!(!app.mouse_flags.normal);
assert!(!app.mouse_flags.visual);
assert!(!app.mouse_flags.insert);
app.dispatch_ex("set mouse");
assert!(app.mouse_flags.normal);
assert!(app.mouse_flags.visual);
assert!(app.mouse_flags.insert);
}
#[test]
fn mouse_flags_as_flags_str_roundtrip() {
for s in ["a", "n", "v", "i", "c", "nvi", "nv", ""] {
let flags = MouseFlags::from_flags(s);
let got = flags.as_flags_str();
let reparsed = MouseFlags::from_flags(&got);
assert_eq!(
flags, reparsed,
"roundtrip failed for {s:?}: as_flags_str={got:?}"
);
}
}
#[test]
fn shift_click_enters_visual_and_extends_selection() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
let mut app = App::new(None, false, None, None).unwrap();
{
use hjkl_engine::BufferEdit;
let buf = app.slots_mut()[0].editor.buffer_mut();
BufferEdit::replace_all(buf, "hello world\nsecond line\nthird\n");
}
if let Some(Some(win)) = app.windows.get_mut(0) {
win.last_rect = Some(Rect::new(0, 1, 80, 20)); win.top_row = 0;
win.top_col = 0;
}
{
let vp = app.slots_mut()[0].editor.host_mut().viewport_mut();
vp.width = 80;
vp.height = 20;
vp.text_width = 80;
vp.top_row = 0;
vp.top_col = 0;
vp.tab_width = 4;
}
assert_eq!(app.active().editor.vim_mode(), VimMode::Normal);
{
let opts = hjkl_engine::Options {
number: false,
relativenumber: false,
..hjkl_engine::Options::default()
};
app.active_mut().editor.apply_options(&opts);
}
let click_screen_row: u16 = 2; let click_screen_col: u16 = 3;
let me = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: click_screen_col,
row: click_screen_row,
modifiers: KeyModifiers::SHIFT,
};
{
use crate::app::mouse;
let zone = mouse::hit_test_zone(&app, me.column, me.row);
if let mouse::Zone::Code {
win_id,
doc_row,
doc_col,
} = zone
{
let current_focus = app.focused_window();
if win_id != current_focus {
app.sync_viewport_from_editor();
app.set_focused_window(win_id);
app.sync_viewport_to_editor();
}
if app.active().editor.vim_mode() != VimMode::Visual {
app.active_mut().editor.mouse_begin_drag();
}
app.active_mut()
.editor
.mouse_extend_drag_doc(doc_row, doc_col);
app.sync_after_engine_mutation();
assert_eq!(
app.active().editor.vim_mode(),
VimMode::Visual,
"Shift+click must enter Visual mode"
);
} else {
panic!("expected Code zone, got {zone:?}");
}
}
}
#[cfg(test)]
mod border_drag_tests {
use super::*;
use crate::app::mouse::SplitOrientation;
use crate::app::{App, SPLIT_MIN_SIZE_COLS, SPLIT_MIN_SIZE_ROWS};
use ratatui::layout::Rect;
fn make_vsplit_with_rect(ratio: f32, total_w: u16, total_h: u16) -> App {
use crate::app::window::{LayoutTree, SplitDir, Tab, Window};
let mut app = App::new(None, false, None, None).unwrap();
let win1 = app.next_window_id;
app.next_window_id += 1;
app.windows.push(Some(Window {
slot: 0,
top_row: 0,
top_col: 0,
cursor_row: 0,
cursor_col: 0,
last_rect: None,
}));
let area = Rect::new(0, 0, total_w, total_h);
let a_w = ((total_w as f32) * ratio).round() as u16;
let a_w = a_w.clamp(1, total_w.saturating_sub(1).max(1));
if let Some(Some(w)) = app.windows.get_mut(0) {
w.last_rect = Some(Rect::new(0, 0, a_w.saturating_sub(1), total_h));
}
if let Some(Some(w)) = app.windows.get_mut(win1) {
w.last_rect = Some(Rect::new(a_w, 0, total_w - a_w, total_h));
}
app.tabs[0] = Tab {
layout: LayoutTree::Split {
dir: SplitDir::Vertical,
ratio,
a: Box::new(LayoutTree::Leaf(0)),
b: Box::new(LayoutTree::Leaf(win1)),
last_rect: Some(area),
},
focused_window: 0,
};
app
}
fn make_hsplit_with_rect(ratio: f32, total_w: u16, total_h: u16) -> App {
use crate::app::window::{LayoutTree, SplitDir, Tab, Window};
let mut app = App::new(None, false, None, None).unwrap();
let win1 = app.next_window_id;
app.next_window_id += 1;
app.windows.push(Some(Window {
slot: 0,
top_row: 0,
top_col: 0,
cursor_row: 0,
cursor_col: 0,
last_rect: None,
}));
let area = Rect::new(0, 0, total_w, total_h);
let a_h = ((total_h as f32) * ratio).round() as u16;
let a_h = a_h.clamp(1, total_h.saturating_sub(1).max(1));
if let Some(Some(w)) = app.windows.get_mut(0) {
w.last_rect = Some(Rect::new(0, 0, total_w, a_h.saturating_sub(1)));
}
if let Some(Some(w)) = app.windows.get_mut(win1) {
w.last_rect = Some(Rect::new(0, a_h, total_w, total_h - a_h));
}
app.tabs[0] = Tab {
layout: LayoutTree::Split {
dir: SplitDir::Horizontal,
ratio,
a: Box::new(LayoutTree::Leaf(0)),
b: Box::new(LayoutTree::Leaf(win1)),
last_rect: Some(area),
},
focused_window: 0,
};
app
}
fn get_split_ratio(app: &App) -> f32 {
match app.layout() {
window::LayoutTree::Split { ratio, .. } => *ratio,
_ => panic!("expected Split"),
}
}
#[test]
fn border_drag_resizes_vertical_split() {
let mut app = make_vsplit_with_rect(0.5, 80, 24);
let ratio_before = get_split_ratio(&app);
app.resize_split_to(SplitOrientation::Vertical, 0, 80, 44);
let ratio_after = get_split_ratio(&app);
assert!(
ratio_after > ratio_before,
"dragging VSplit right must grow ratio: before={ratio_before} after={ratio_after}"
);
let expected = 44.0f32 / 80.0;
assert!(
(ratio_after - expected).abs() < 0.02,
"ratio should be ~{expected:.2}, got {ratio_after:.4}"
);
}
#[test]
fn border_drag_resizes_horizontal_split() {
let mut app = make_hsplit_with_rect(0.5, 80, 24);
let ratio_before = get_split_ratio(&app);
app.resize_split_to(SplitOrientation::Horizontal, 0, 24, 8);
let ratio_after = get_split_ratio(&app);
assert!(
ratio_after < ratio_before,
"dragging HSplit up must shrink ratio: before={ratio_before} after={ratio_after}"
);
let expected = 8.0f32 / 24.0;
assert!(
(ratio_after - expected).abs() < 0.02,
"ratio should be ~{expected:.2}, got {ratio_after:.4}"
);
}
#[test]
fn border_double_click_equalizes_split() {
let mut app = make_vsplit_with_rect(0.3, 80, 24);
if let window::LayoutTree::Split { ratio, .. } = app.layout_mut() {
*ratio = 0.3;
}
assert!((get_split_ratio(&app) - 0.3).abs() < 1e-4, "precondition");
app.equalize_split();
let ratio_after = get_split_ratio(&app);
assert!(
(ratio_after - 0.5).abs() < 1e-4,
"equalize_split must set ratio to 0.5, got {ratio_after}"
);
}
#[test]
fn border_drag_respects_min_size_vertical() {
let mut app = make_vsplit_with_rect(0.5, 80, 24);
app.resize_split_to(SplitOrientation::Vertical, 0, 80, 2);
let ratio = get_split_ratio(&app);
let min_ratio = SPLIT_MIN_SIZE_COLS as f32 / 80.0;
assert!(
ratio >= min_ratio - 1e-4,
"ratio must be >= min ({min_ratio:.3}), got {ratio:.4}"
);
}
#[test]
fn border_drag_respects_min_size_horizontal() {
let mut app = make_hsplit_with_rect(0.5, 80, 24);
app.resize_split_to(SplitOrientation::Horizontal, 0, 24, 1);
let ratio = get_split_ratio(&app);
let min_ratio = SPLIT_MIN_SIZE_ROWS as f32 / 24.0;
assert!(
ratio >= min_ratio - 1e-4,
"ratio must be >= min ({min_ratio:.3}), got {ratio:.4}"
);
}
#[test]
fn border_drag_respects_min_size_other_side() {
let mut app = make_vsplit_with_rect(0.5, 80, 24);
app.resize_split_to(SplitOrientation::Vertical, 0, 80, 79);
let ratio = get_split_ratio(&app);
let max_ratio = (80 - SPLIT_MIN_SIZE_COLS - 1) as f32 / 80.0;
assert!(
ratio <= max_ratio + 1e-4,
"ratio must be <= max ({max_ratio:.3}) to leave room for b, got {ratio:.4}"
);
}
#[test]
fn border_drag_no_active_split_is_noop() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(app.border_drag.is_none(), "border_drag must start None");
app.resize_split_to(SplitOrientation::Vertical, 0, 80, 40);
assert!(app.border_drag.is_none());
}
#[test]
fn dismiss_hover_popup_on_click_clears_state() {
use crate::hover_popup::HoverPopup;
use std::time::Instant;
let mut app = App::new(None, false, None, None).unwrap();
app.hover_popup = Some(HoverPopup::new("stale content".to_string(), (50, 5)));
app.hover_timer = Some(HoverTimer {
cell: (50, 5),
started_at: Instant::now(),
request_sent: true,
});
app.dismiss_hover_popup_on_click();
assert!(
app.hover_popup.is_none(),
"hover_popup must be cleared on mouse click — leaving stale popups \
causes the right-edge garbage bug (right-click → Go to Definition repro)"
);
assert!(
app.hover_timer.is_none(),
"hover_timer must also be cleared so a subsequent rest re-arms cleanly"
);
}
#[test]
fn screen_rect_includes_top_bar_when_multiple_slots() {
let path_a = std::env::temp_dir().join("hjkl_screen_rect_a.txt");
let path_b = std::env::temp_dir().join("hjkl_screen_rect_b.txt");
for p in [&path_a, &path_b] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
{
let vp = app.slots_mut()[0].editor.host_mut().viewport_mut();
vp.width = 80;
vp.height = 22; }
let single = app.screen_rect();
assert_eq!(
single.height,
22 + STATUS_LINE_HEIGHT,
"single-slot screen height must skip the (absent) top bar"
);
app.dispatch_ex(&format!("e {}", path_b.display()));
let active = app.focused_slot_idx();
{
let vp = app.slots_mut()[active].editor.host_mut().viewport_mut();
vp.width = 80;
vp.height = 22;
}
let multi = app.screen_rect();
assert_eq!(
multi.height,
TOP_BAR_HEIGHT + 22 + STATUS_LINE_HEIGHT,
"multi-slot screen height must include the top bar row \
(otherwise context-menu hover near the bottom maps to the wrong item)"
);
for p in [&path_a, &path_b] {
let _ = std::fs::remove_file(p);
}
}
fn make_app_with_window(content: &str, area: ratatui::layout::Rect) -> App {
use hjkl_engine::BufferEdit;
let mut app = App::new(None, false, None, None).unwrap();
{
let buf = app.slots_mut()[0].editor.buffer_mut();
BufferEdit::replace_all(buf, content);
}
if let Some(Some(win)) = app.windows.get_mut(0) {
win.last_rect = Some(area);
win.top_row = 0;
win.top_col = 0;
}
{
let vp = app.slots_mut()[0].editor.host_mut().viewport_mut();
vp.width = area.width;
vp.height = area.height;
vp.text_width = area.width;
vp.top_row = 0;
vp.top_col = 0;
vp.tab_width = 4;
}
app
}
#[test]
fn move_cursor_for_right_click_moves_cursor_to_click() {
let mut app = make_app_with_window(
"line one\nline two\nline three\nline four\nline five",
ratatui::layout::Rect::new(0, 0, 80, 24),
);
app.active_mut().editor.set_cursor_doc(0, 0);
assert_eq!(app.active().editor.cursor(), (0, 0));
app.move_cursor_for_right_click(12, 2);
assert_eq!(
app.active().editor.cursor(),
(2, 8),
"right-click must move cursor to clicked doc position"
);
}
#[test]
fn move_cursor_for_right_click_preserves_visual_selection() {
use hjkl_engine::VimMode;
let mut app = make_app_with_window(
"line one\nline two\nline three\nline four\nline five",
ratatui::layout::Rect::new(0, 0, 80, 24),
);
app.active_mut().editor.set_cursor_doc(0, 0);
app.active_mut().editor.enter_visual_char();
app.active_mut().editor.set_cursor_doc(0, 4);
let before = app.active().editor.cursor();
assert_eq!(app.active().editor.vim_mode(), VimMode::Visual);
app.move_cursor_for_right_click(12, 3);
assert_eq!(
app.active().editor.cursor(),
before,
"right-click with active visual selection must not move cursor"
);
assert_eq!(
app.active().editor.vim_mode(),
VimMode::Visual,
"visual mode must survive the right-click"
);
}
#[test]
fn move_cursor_for_right_click_in_gutter_goes_to_col_zero() {
let mut app = make_app_with_window(
"first\nsecond\nthird\nfourth\nfifth",
ratatui::layout::Rect::new(0, 0, 80, 24),
);
app.active_mut().editor.set_cursor_doc(0, 2);
app.move_cursor_for_right_click(0, 2);
assert_eq!(
app.active().editor.cursor(),
(2, 0),
"gutter right-click moves cursor to (clicked_row, 0)"
);
}
#[test]
fn move_cursor_for_right_click_outside_window_is_noop() {
let mut app = make_app_with_window(
"first\nsecond\nthird",
ratatui::layout::Rect::new(0, 0, 80, 24),
);
app.active_mut().editor.set_cursor_doc(1, 3);
let before = app.active().editor.cursor();
app.move_cursor_for_right_click(10, 30);
assert_eq!(
app.active().editor.cursor(),
before,
"right-click outside any window must not move the cursor"
);
}
#[test]
fn backspace_on_empty_command_prompt_dismisses() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = App::new(None, false, None, None).unwrap();
app.open_command_prompt();
assert!(app.command_field.is_some(), "prompt should be open");
app.handle_command_field_key(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE));
app.handle_command_field_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
assert!(
app.command_field.is_some(),
"first backspace cleared the char; prompt still open"
);
app.handle_command_field_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
assert!(
app.command_field.is_none(),
"second backspace on empty prompt must dismiss it (neovim parity)"
);
}
#[test]
fn backspace_on_empty_forward_search_prompt_dismisses() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = App::new(None, false, None, None).unwrap();
app.open_search_prompt(SearchDir::Forward);
assert!(app.search_field.is_some(), "search prompt should be open");
app.handle_search_field_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
app.handle_search_field_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
assert!(app.search_field.is_some(), "still open while empty");
app.handle_search_field_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
assert!(
app.search_field.is_none(),
"backspace on empty search prompt must dismiss"
);
}
#[test]
fn backspace_on_empty_backward_search_prompt_dismisses() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = App::new(None, false, None, None).unwrap();
app.open_search_prompt(SearchDir::Backward);
assert!(app.search_field.is_some());
app.handle_search_field_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
assert!(
app.search_field.is_none(),
"backspace on freshly-opened (empty) backward-search prompt must dismiss"
);
}
#[test]
fn middle_click_on_buffer_line_closes_that_buffer() {
let path_a = std::env::temp_dir().join("hjkl_mclick_bl_a.txt");
let path_b = std::env::temp_dir().join("hjkl_mclick_bl_b.txt");
let path_c = std::env::temp_dir().join("hjkl_mclick_bl_c.txt");
for p in [&path_a, &path_b, &path_c] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("e {}", path_b.display()));
app.dispatch_ex(&format!("e {}", path_c.display()));
if let Some(Some(win)) = app.windows.get_mut(0) {
win.last_rect = Some(ratatui::layout::Rect::new(0, 0, 200, 24));
}
assert_eq!(app.slots.len(), 3);
let ranges = crate::app::mouse::buffer_line_x_ranges(&app, 200);
assert!(ranges.len() >= 3);
let first_col = ranges[0].0;
app.middle_click(first_col, 0);
assert_eq!(
app.slots.len(),
2,
"middle-click on buffer line entry must close that buffer"
);
for p in [&path_a, &path_b, &path_c] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn middle_click_on_tab_closes_that_tab() {
let path_a = std::env::temp_dir().join("hjkl_mclick_tab_a.txt");
let path_b = std::env::temp_dir().join("hjkl_mclick_tab_b.txt");
for p in [&path_a, &path_b] {
std::fs::write(p, "x\n").unwrap();
}
let mut app = App::new(Some(path_a.clone()), false, None, None).unwrap();
app.dispatch_ex(&format!("tabnew {}", path_b.display()));
if let Some(Some(win)) = app.windows.get_mut(0) {
win.last_rect = Some(ratatui::layout::Rect::new(0, 0, 200, 24));
}
if let Some(Some(win)) = app.windows.get_mut(1) {
win.last_rect = Some(ratatui::layout::Rect::new(0, 0, 200, 24));
}
assert_eq!(app.tabs.len(), 2);
let ranges = crate::app::mouse::tab_x_ranges(&app, 200);
assert_eq!(ranges.len(), 2);
let first_col = ranges[0].0;
app.middle_click(first_col, 0);
assert_eq!(
app.tabs.len(),
1,
"middle-click on tab entry must close that tab"
);
for p in [&path_a, &path_b] {
let _ = std::fs::remove_file(p);
}
}
#[test]
fn middle_click_outside_zones_is_noop() {
let mut app = make_app_with_window(
"alpha\nbeta\ngamma",
ratatui::layout::Rect::new(0, 0, 80, 24),
);
let slots_before = app.slots.len();
let tabs_before = app.tabs.len();
app.middle_click(10, 30);
assert_eq!(app.slots.len(), slots_before);
assert_eq!(app.tabs.len(), tabs_before);
}
#[test]
fn tick_hover_timer_suppressed_while_context_menu_open() {
use crate::menu::{ContextMenu, MenuAction, MenuItem};
use std::time::{Duration, Instant};
let mut app = App::new(None, false, None, None).unwrap();
app.hover_timer = Some(HoverTimer {
cell: (10, 5),
started_at: Instant::now() - Duration::from_millis(800),
request_sent: false,
});
let items = vec![MenuItem::new("Cut", MenuAction::Cut, None)];
app.context_menu = Some(ContextMenu::new(items, (5, 5)));
assert!(
app.overlay_active(),
"overlay_active must report true when context_menu is set"
);
app.tick_hover_timer();
assert!(
app.hover_popup.is_none(),
"hover_popup must remain unset while a context menu is open"
);
assert!(
app.hover_timer.is_none(),
"hover_timer must be dropped under overlay so it doesn't fire the moment the overlay closes"
);
}
#[test]
fn handle_hover_at_mouse_response_dropped_under_overlay() {
use crate::menu::{ContextMenu, MenuAction, MenuItem};
use std::time::Instant;
let mut app = App::new(None, false, None, None).unwrap();
app.hover_timer = Some(HoverTimer {
cell: (10, 5),
started_at: Instant::now(),
request_sent: true,
});
let items = vec![MenuItem::new("Cut", MenuAction::Cut, None)];
app.context_menu = Some(ContextMenu::new(items, (5, 5)));
let response: Result<serde_json::Value, hjkl_lsp::RpcError> = Ok(serde_json::json!({
"contents": { "kind": "plaintext", "value": "stale hover text" }
}));
app.handle_hover_at_mouse_response(0, (0, 0), response);
assert!(
app.hover_popup.is_none(),
"hover_popup must not be created when an overlay was open at response time"
);
}
#[test]
fn overlay_active_reports_each_overlay_kind() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(!app.overlay_active(), "fresh app has no overlays");
let items = vec![crate::menu::MenuItem::new(
"x",
crate::menu::MenuAction::Cut,
None,
)];
app.context_menu = Some(crate::menu::ContextMenu::new(items, (0, 0)));
assert!(app.overlay_active());
app.context_menu = None;
assert!(!app.overlay_active());
}
#[test]
fn dismiss_hover_popup_on_click_is_idempotent_when_no_popup() {
let mut app = App::new(None, false, None, None).unwrap();
assert!(app.hover_popup.is_none());
assert!(app.hover_timer.is_none());
app.dismiss_hover_popup_on_click();
assert!(app.hover_popup.is_none());
assert!(app.hover_timer.is_none());
}
}