use std::{cell::RefCell, rc::Rc}; use gpui::{ Context, IntoElement, Modifiers, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, }; use gpui_native_data::sidebar::{Sidebar, SidebarEntry}; use gpui_native_theme::Theme; type Changes = Rc>>>; struct Harness { selected: Option, changes: Changes, } impl Render for Harness { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let changes = self.changes.clone(); let view = cx.entity(); div().size_full().child( Sidebar::new("test-sidebar") .selected(self.selected) .on_change(move |next, _, cx| { changes.borrow_mut().push(next); view.update(cx, |harness, cx| { harness.selected = next; cx.notify(); }); }) .entries(|_, _| { vec![ SidebarEntry::header("Favorites"), // 0: never selectable SidebarEntry::row("Documents"), // 1 SidebarEntry::row("Locked").disabled(true), // 2 SidebarEntry::header("Locations"), // 3: never selectable SidebarEntry::row("Trash"), // 4 ] }), ) } } /// Entries stack from the top-left: the header band (~32 px) then ~34 px row /// bands, so clicks at `(50, 16)` hit the header, `(50, 48)` Documents, /// `(50, 82)` Locked, `(50, 116)` the second header, `(50, 150)` Trash. fn sidebar_window() -> (TestAppContext, VisualTestContext, Changes) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let changes = Rc::new(RefCell::new(Vec::new())); let harness_changes = changes.clone(); let window = cx.add_window(move |_, _| Harness { selected: None, changes: harness_changes, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, changes) } fn draw(visual: &mut VisualTestContext) { visual.update(|window, cx| { window.draw(cx).clear(cx); }); } fn click(visual: &mut VisualTestContext, position: gpui::Point) { visual.simulate_mouse_down(position, MouseButton::Left, Modifiers::default()); visual.simulate_mouse_up(position, MouseButton::Left, Modifiers::default()); } #[test] fn clicking_a_row_selects_it_and_hands_the_sidebar_the_keyboard() { let (_cx, mut visual, changes) = sidebar_window(); // The header band is not a row: clicking it reports nothing. click(&mut visual, point(px(50.), px(16.))); draw(&mut visual); assert!(changes.borrow().is_empty()); click(&mut visual, point(px(50.), px(48.))); assert_eq!(*changes.borrow(), vec![Some(1)]); // The click focused the sidebar; the arrows move from here. visual.simulate_keystrokes("down"); draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(1), Some(4)]); } #[test] fn clicking_a_disabled_row_reports_nothing() { let (_cx, mut visual, changes) = sidebar_window(); click(&mut visual, point(px(50.), px(82.))); draw(&mut visual); assert!(changes.borrow().is_empty()); } #[test] fn keyboard_navigation_skips_headers_disabled_rows_and_never_wraps() { let (_cx, mut visual, changes) = sidebar_window(); click(&mut visual, point(px(50.), px(48.))); // Documents, focuses visual.simulate_keystrokes("down"); // over Locked → Trash visual.simulate_keystrokes("down"); // over the second header; no wrap draw(&mut visual); assert_eq!(*changes.borrow(), vec![Some(1), Some(4)]); visual.simulate_keystrokes("up"); // over Locked → Documents visual.simulate_keystrokes("end"); // last selectable row visual.simulate_keystrokes("home"); // first selectable row draw(&mut visual); assert_eq!( *changes.borrow(), vec![Some(1), Some(4), Some(1), Some(4), Some(1)] ); }