use crate::harness;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use gpui::{
point, prelude::*, px, Font, FontFeatures, FontStyle, FontWeight, KeyUpEvent, Keystroke,
Modifiers, MouseButton, SharedString, TestAppContext, VisualTestContext,
};
use herogpui_components::{
calendar::{Date, CALENDAR_WIDTH},
Button, Calendar, CalendarCellState, CalendarState, DateConstraints, DateRangeState, Dropdown,
InteractiveState, ListBox, ListBoxItem, Menu, MenuItem, RadioGroup, RadioOption,
RadioOptionState, RangeCalendar, RangeCalendarCellState, SelectionMode, Tag, TagGroup,
VisibleDuration, Weekday,
};
use harness::{click, events, open_host, press};
fn flush_frame(cx: &mut VisualTestContext) {
cx.update(|window, _| window.refresh());
}
type Interactives = Rc<RefCell<HashMap<String, InteractiveState>>>;
fn record_interactive(map: &Interactives, key: &SharedString, state: InteractiveState) {
map.borrow_mut().insert(key.to_string(), state);
}
fn state_of(map: &Interactives, key: &str) -> InteractiveState {
map.borrow().get(key).copied().unwrap_or_default()
}
type RadioStates = Rc<RefCell<HashMap<String, RadioOptionState>>>;
fn record_radio_state(map: &RadioStates, key: &SharedString, state: RadioOptionState) {
map.borrow_mut().insert(key.to_string(), state);
}
fn radio_state_of(map: &RadioStates, key: &str) -> RadioOptionState {
map.borrow().get(key).copied().unwrap_or_default()
}
fn record_cell(map: &Rc<RefCell<HashMap<String, CalendarCellState>>>, state: &CalendarCellState) {
map.borrow_mut()
.insert(state.date.format_iso(), state.clone());
}
fn cell_of(map: &Rc<RefCell<HashMap<String, CalendarCellState>>>, key: &str) -> CalendarCellState {
map.borrow()
.get(key)
.cloned()
.unwrap_or_else(|| panic!("cell for {key} never rendered"))
}
fn record_range_cell(
map: &Rc<RefCell<HashMap<String, RangeCalendarCellState>>>,
state: &RangeCalendarCellState,
) {
map.borrow_mut()
.insert(state.date.format_iso(), state.clone());
}
fn range_cell_of(
map: &Rc<RefCell<HashMap<String, RangeCalendarCellState>>>,
key: &str,
) -> RangeCalendarCellState {
map.borrow()
.get(key)
.cloned()
.unwrap_or_else(|| panic!("cell for {key} never rendered"))
}
fn sorted_join(keys: &HashSet<SharedString>) -> String {
let mut names: Vec<String> = keys.iter().map(ToString::to_string).collect();
names.sort();
names.join(",")
}
fn text_width(system: &gpui::WindowTextSystem, text: &str, size: f32, weight: FontWeight) -> f32 {
let run = gpui::TextRun {
len: text.len(),
font: Font {
family: ".SystemUIFont".into(),
features: FontFeatures::default(),
weight,
style: FontStyle::default(),
fallbacks: None,
},
color: gpui::black(),
background_color: None,
underline: None,
strikethrough: None,
};
let line = system.shape_line(text.to_owned().into(), px(size), &[run], None);
f32::from(line.width)
}
fn cal_col_x(col: usize) -> f32 {
let cell_w = (f32::from(CALENDAR_WIDTH) - 12.) / 7.;
col as f32 * (cell_w + 2.) + cell_w / 2.
}
fn cal_row_y(row: usize) -> f32 {
74. + row as f32 * 38.
}
fn cal_day(year: i32, month: u32, day: u32) -> (f32, f32) {
let lead = DateConstraints::new().lead_cells(year, month);
let idx = day as usize + lead - 1;
(cal_col_x(idx % 7), cal_row_y(idx / 7))
}
fn range_col_x(col: usize) -> f32 {
19. + 38. * col as f32
}
fn range_row_y(row: usize) -> f32 {
75. + 40. * row as f32
}
fn range_day(year: i32, month: u32, day: u32) -> (f32, f32) {
let lead = DateConstraints::new().lead_cells(year, month);
let idx = day as usize + lead - 1;
(range_col_x(idx % 7), range_row_y(idx / 7))
}
#[gpui::test]
fn list_box_item_content_renders_at_all(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let _cx = open_host(cx, move || {
let record = record.clone();
ListBox::new(
"rp-lb-render",
vec![
ListBoxItem::new("alpha", "Alpha"),
ListBoxItem::new("beta", "Beta"),
ListBoxItem::new("gamma", "Gamma"),
],
)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
for key in ["alpha", "beta", "gamma"] {
let state = state_of(&recorded, key);
assert!(
!state.is_selected && !state.is_focused,
"row {key} must have recorded the idle state on the first frame"
);
}
}
#[gpui::test]
fn list_box_item_content_tracks_selection_and_cursor(cx: &mut TestAppContext) {
let held = Rc::new(RefCell::new(HashSet::<SharedString>::new()));
let recorded = Rc::new(RefCell::new(HashMap::new()));
let changes = events();
let changed = changes.clone();
let record = recorded.clone();
let cx = open_host(cx, move || {
let held = held.clone();
let record = record.clone();
let changes = changes.clone();
let selected_now = held.borrow().clone();
gpui::div()
.flex()
.flex_col()
.child(
ListBox::new(
"rp-lb-state",
vec![
ListBoxItem::new("alpha", "Alpha"),
ListBoxItem::new("beta", "Beta"),
],
)
.selection_mode(SelectionMode::Single)
.selected_keys(selected_now)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.on_selection_change(move |keys, window, _| {
changes.borrow_mut().push(sorted_join(keys));
*held.borrow_mut() = keys.clone();
window.refresh();
}),
)
.child(Button::new("after-rp-list-box").label("After"))
.into_any_element()
});
cx.update(|window, _| window.activate_window());
cx.update(|_, cx| herogpui_components::extend::set_focus_visible(true, cx));
flush_frame(cx);
press(cx, "tab");
press(cx, "down");
flush_frame(cx);
let beta = state_of(&recorded, "beta");
assert!(
beta.is_focused && beta.is_focus_visible && !beta.is_selected,
"the frame after Down must focus and ring the second row"
);
assert!(
!state_of(&recorded, "alpha").is_focused,
"the first row must have lost the cursor"
);
press(cx, "enter");
flush_frame(cx);
assert!(
state_of(&recorded, "beta").is_selected,
"the frame after Enter must see row beta selected"
);
assert_eq!(changed.borrow().as_slice(), ["beta"]);
press(cx, "up");
flush_frame(cx);
assert!(
state_of(&recorded, "alpha").is_focused,
"the cursor must have moved back to the first row"
);
assert!(
!state_of(&recorded, "beta").is_focused,
"the second row must have lost the cursor"
);
cx.simulate_mouse_move(
point(px(60.), px(22.)),
None::<MouseButton>,
Modifiers::none(),
);
flush_frame(cx);
assert!(
!state_of(&recorded, "alpha").is_hovered,
"ListBox does not hand the hover to the item_content closure"
);
press(cx, "tab");
flush_frame(cx);
assert!(
["alpha", "beta"].iter().all(|key| {
let state = state_of(&recorded, key);
!state.is_focused && !state.is_focus_visible
}),
"moving focus out of the ListBox must clear every render-prop focus and ring state"
);
}
#[gpui::test]
fn list_box_item_content_pointer_selection_is_inert(cx: &mut TestAppContext) {
let held = Rc::new(RefCell::new(HashSet::<SharedString>::new()));
let recorded = Rc::new(RefCell::new(HashMap::new()));
let changes = events();
let changed = changes.clone();
let record = recorded.clone();
let cx = open_host(cx, move || {
let held = held.clone();
let record = record.clone();
let changes = changes.clone();
let selected_now = held.borrow().clone();
ListBox::new(
"rp-lb-click",
vec![
ListBoxItem::new("alpha", "Alpha"),
ListBoxItem::new("beta", "Beta"),
],
)
.selection_mode(SelectionMode::Single)
.selected_keys(selected_now)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.on_selection_change(move |keys, window, _| {
changes.borrow_mut().push(sorted_join(keys));
*held.borrow_mut() = keys.clone();
window.refresh();
})
.into_any_element()
});
click(cx, 60., 22.);
flush_frame(cx);
assert_eq!(
changed.borrow().as_slice(),
["alpha"],
"a click on the row must select it"
);
assert!(
state_of(&recorded, "alpha").is_selected,
"the item_content closure must see the pick"
);
}
#[gpui::test]
fn list_box_item_content_sees_the_press(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
ListBox::new(
"rp-lb-press",
vec![
ListBoxItem::new("alpha", "Alpha"),
ListBoxItem::new("beta", "Beta"),
],
)
.selection_mode(SelectionMode::Single)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let centre = point(px(60.), px(22.));
cx.simulate_mouse_move(centre, None::<MouseButton>, Modifiers::none());
cx.simulate_mouse_down(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert!(
state_of(&recorded, "alpha").is_pressed,
"the frame after the down must hand the press to the closure"
);
cx.simulate_mouse_up(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
press(cx, "tab");
cx.simulate_keystrokes("enter");
flush_frame(cx);
assert!(
state_of(&recorded, "alpha").is_pressed,
"the frame after Enter down must hand the focused row's keyboard press to the closure"
);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse("enter").unwrap(),
});
flush_frame(cx);
assert!(
!state_of(&recorded, "alpha").is_pressed,
"Enter up must release the focused row's keyboard press"
);
}
#[gpui::test]
fn list_box_item_content_drops_press_when_focused_row_becomes_disabled(cx: &mut TestAppContext) {
let disabled = Rc::new(RefCell::new(false));
let disabled_for_view = disabled.clone();
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let actions = events();
let acted = actions.clone();
let cx = open_host(cx, move || {
let record = record.clone();
let item = ListBoxItem::new("alpha", "Alpha").is_disabled(*disabled_for_view.borrow());
ListBox::new("rp-lb-disable-press", vec![item])
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.on_action({
let actions = actions.clone();
move |key, _, _| actions.borrow_mut().push(key.to_string())
})
.into_any_element()
});
cx.update(|window, _| window.activate_window());
flush_frame(cx);
press(cx, "tab");
flush_frame(cx);
assert!(state_of(&recorded, "alpha").is_focused);
*disabled.borrow_mut() = true;
flush_frame(cx);
cx.simulate_keystrokes("enter");
flush_frame(cx);
let alpha = state_of(&recorded, "alpha");
assert!(alpha.is_disabled && !alpha.is_focused && !alpha.is_pressed);
assert!(
acted.borrow().is_empty(),
"a stale cursor must not activate a newly disabled row"
);
}
#[gpui::test]
fn list_box_item_content_is_not_pressed_when_row_has_no_behavior(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
ListBox::new(
"rp-lb-inert-press",
vec![ListBoxItem::new("alpha", "Alpha")],
)
.selection_mode(SelectionMode::None)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let centre = point(px(60.), px(22.));
cx.simulate_mouse_down(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert!(!state_of(&recorded, "alpha").is_pressed);
cx.simulate_mouse_up(centre, MouseButton::Left, Modifiers::none());
press(cx, "tab");
cx.simulate_keystrokes("space");
flush_frame(cx);
assert!(!state_of(&recorded, "alpha").is_pressed);
}
#[gpui::test]
fn dropdown_forwards_item_content_to_its_composed_menu(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
Dropdown::new(
"rp-dropdown-item",
Button::new("rp-dropdown-item-trigger").label("Open"),
vec![MenuItem::new("one", "One"), MenuItem::new("two", "Two")],
true,
)
.selection_mode(SelectionMode::Multiple)
.default_selected_keys([SharedString::from("one")])
.disabled_keys([SharedString::from("two")])
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
flush_frame(cx);
let one = state_of(&recorded, "one");
let two = state_of(&recorded, "two");
assert!(
one.is_selected && !one.is_disabled && !two.is_selected && two.is_disabled,
"the composed menu must hand each item closure its selection and disabled state"
);
}
#[gpui::test]
fn dropdown_forwards_indicator_content_to_its_composed_menu(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::<String, (bool, bool)>::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
Dropdown::new(
"rp-dropdown-indicator",
Button::new("rp-dropdown-indicator-trigger").label("Open"),
vec![MenuItem::new("one", "One"), MenuItem::new("two", "Two")],
true,
)
.selection_mode(SelectionMode::Multiple)
.default_selected_keys([SharedString::from("one")])
.indicator_content(move |key, is_selected, is_indeterminate| {
record
.borrow_mut()
.insert(key.to_string(), (is_selected, is_indeterminate));
gpui::div().size(px(16.)).into_any_element()
})
.into_any_element()
});
flush_frame(cx);
assert_eq!(
recorded.borrow().clone(),
HashMap::from([
("one".to_owned(), (true, false)),
("two".to_owned(), (false, false)),
]),
"indicator render state must reach every composed dropdown item"
);
press(cx, "down");
press(cx, "down");
press(cx, "enter");
flush_frame(cx);
assert_eq!(
recorded.borrow().clone(),
HashMap::from([
("one".to_owned(), (true, false)),
("two".to_owned(), (true, false)),
]),
"the composed indicator closure must observe live uncontrolled selection"
);
}
#[gpui::test]
fn dropdown_forwards_render_content_into_submenus(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let indicators = Rc::new(RefCell::new(HashMap::<String, (bool, bool)>::new()));
let record = recorded.clone();
let indicator_record = indicators.clone();
let cx = open_host(cx, move || {
let record = record.clone();
let indicator_record = indicator_record.clone();
Dropdown::new(
"rp-dropdown-submenu",
Button::new("rp-dropdown-submenu-trigger").label("Open"),
vec![MenuItem::new("parent", "Parent").submenu(vec![MenuItem::new("child", "Child")])],
true,
)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.indicator_content(move |key, is_selected, is_indeterminate| {
indicator_record
.borrow_mut()
.insert(key.to_string(), (is_selected, is_indeterminate));
gpui::div().size(px(16.)).into_any_element()
})
.into_any_element()
});
click(cx, 40., 64.);
flush_frame(cx);
assert!(
recorded.borrow().contains_key("child"),
"the root item closure must reach a submenu item"
);
assert_eq!(
indicators.borrow().get("child"),
Some(&(false, false)),
"the root indicator closure must reach the submenu item state"
);
}
#[gpui::test]
fn menu_item_content_renders_at_all(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let _cx = open_host(cx, move || {
let record = record.clone();
Menu::new(
"render-props-menu-press",
vec![MenuItem::new("one", "One"), MenuItem::new("two", "Two")],
)
.id("rp-menu-render")
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
for key in ["one", "two"] {
let state = state_of(&recorded, key);
assert!(
!state.is_selected && !state.is_focused,
"row {key} must have recorded the idle state on the first frame"
);
}
}
#[gpui::test]
fn menu_item_content_tracks_selection_and_cursor(cx: &mut TestAppContext) {
let held = Rc::new(RefCell::new(Vec::<SharedString>::new()));
let recorded = Rc::new(RefCell::new(HashMap::new()));
let changes = events();
let changed = changes.clone();
let record = recorded.clone();
let cx = open_host(cx, move || {
let held = held.clone();
let record = record.clone();
let changes = changes.clone();
let selected_now = held.borrow().clone();
Menu::new(
"render-props-menu-selection",
vec![
MenuItem::new("one", "One"),
MenuItem::new("two", "Two"),
MenuItem::new("three", "Three"),
],
)
.id("rp-menu-state")
.selection_mode(SelectionMode::Multiple)
.selected_keys(selected_now)
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.on_selection_change(move |keys, window, _| {
let joined = keys
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
changes.borrow_mut().push(joined);
*held.borrow_mut() = keys.to_vec();
window.refresh();
})
.into_any_element()
});
press(cx, "down");
flush_frame(cx);
assert!(
state_of(&recorded, "one").is_focused,
"the frame after Down must see the cursor on the first row"
);
press(cx, "enter");
flush_frame(cx);
let one = state_of(&recorded, "one");
assert!(one.is_selected, "the picked row must report selected");
for key in ["two", "three"] {
let state = state_of(&recorded, key);
assert!(
!state.is_selected && !state.is_indeterminate,
"{key} must stay idle rather than inventing indeterminate item state"
);
}
assert_eq!(changed.borrow().as_slice(), ["one"]);
click(cx, 60., 60.);
flush_frame(cx);
assert!(
state_of(&recorded, "two").is_selected,
"the clicked row must report selected"
);
assert!(
state_of(&recorded, "one").is_selected,
"the earlier pick must survive the second one"
);
assert_eq!(
changed.borrow().as_slice(),
["one", "one,two"],
"the second report must still contain the first pick"
);
press(cx, "down");
press(cx, "down");
flush_frame(cx);
assert!(
state_of(&recorded, "three").is_focused,
"the cursor must reach the third row"
);
assert!(
!state_of(&recorded, "one").is_focused,
"the cursor must have left the first row"
);
cx.simulate_mouse_move(
point(px(60.), px(98.)),
None::<MouseButton>,
Modifiers::none(),
);
flush_frame(cx);
assert!(
!state_of(&recorded, "three").is_hovered,
"Menu does not hand the hover to the item_content closure"
);
}
#[gpui::test]
fn menu_item_content_sees_the_press(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
Menu::new(
"render-props-menu-disabled",
vec![MenuItem::new("one", "One"), MenuItem::new("two", "Two")],
)
.id("rp-menu-press")
.disabled_keys([SharedString::from("two")])
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let centre = point(px(60.), px(22.));
cx.simulate_mouse_move(centre, None::<MouseButton>, Modifiers::none());
cx.simulate_mouse_down(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert!(
state_of(&recorded, "one").is_pressed,
"the frame after the down must hand the press to the closure"
);
cx.simulate_mouse_up(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
press(cx, "home");
cx.simulate_keystrokes("space");
flush_frame(cx);
assert!(
state_of(&recorded, "one").is_pressed,
"the frame after Space down must hand the focused menu row's keyboard press to the closure"
);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse("space").unwrap(),
});
flush_frame(cx);
assert!(
!state_of(&recorded, "one").is_pressed,
"Space up must release the focused menu row's keyboard press"
);
let disabled = point(px(60.), px(60.));
cx.simulate_mouse_down(disabled, MouseButton::Left, Modifiers::none());
flush_frame(cx);
assert!(
!state_of(&recorded, "two").is_pressed,
"a disabled menu row must not report a pointer press"
);
}
#[gpui::test]
fn menu_item_content_pointer_press_moves_focus(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
Menu::new(
"render-props-menu-pointer-focus",
vec![MenuItem::new("one", "One"), MenuItem::new("two", "Two")],
)
.id("rp-menu-pointer-focus")
.item_content(move |key, state| {
record_interactive(&record, key, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let centre = point(px(60.), px(60.));
cx.simulate_mouse_move(centre, None::<MouseButton>, Modifiers::none());
flush_frame(cx);
assert!(
state_of(&recorded, "two").is_focused,
"pointer hover must move the menu's focused key to its row"
);
cx.simulate_mouse_down(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
let two = state_of(&recorded, "two");
assert!(
two.is_pressed,
"the press must reach the second row's closure"
);
assert!(
two.is_focused,
"a pointer press must move the menu's focused key to its row"
);
}
#[gpui::test]
fn tag_group_tag_content_renders_at_all(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let _cx = open_host(cx, move || {
let record = record.clone();
TagGroup::new(
"rp-tg-render",
vec![
Tag::new("alpha", "Alpha"),
Tag::new("beta", "Beta"),
Tag::new("gamma", "Gamma"),
],
)
.tag_content(move |tag, state| {
record_interactive(&record, tag.key(), state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let alpha = state_of(&recorded, "alpha");
assert!(!alpha.is_focused, "a cursor alone is not window focus");
assert!(!alpha.is_selected, "nothing is selected on the first frame");
for key in ["beta", "gamma"] {
let state = state_of(&recorded, key);
assert!(
!state.is_focused && !state.is_selected,
"{key} must be idle on the first frame"
);
}
}
#[gpui::test]
fn disabling_tag_group_tag_content_clears_hover_and_press(cx: &mut TestAppContext) {
let disabled = Rc::new(RefCell::new(false));
let disabled_for_view = disabled.clone();
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
TagGroup::new(
"rp-tg-disabled-state",
vec![Tag::new("alpha", "Alpha").is_disabled(*disabled_for_view.borrow())],
)
.selection_mode(SelectionMode::Single)
.tag_content(move |tag, state| {
record_interactive(&record, tag.key(), state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let centre = point(px(28.), px(14.));
cx.simulate_mouse_move(centre, None::<MouseButton>, Modifiers::none());
cx.simulate_mouse_down(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
let active = state_of(&recorded, "alpha");
assert!(active.is_hovered && active.is_pressed && !active.is_disabled);
*disabled.borrow_mut() = true;
flush_frame(cx);
let alpha = state_of(&recorded, "alpha");
assert!(
alpha.is_disabled && !alpha.is_hovered && !alpha.is_pressed,
"a disabled tag must expose disabled state without pointer interaction"
);
cx.simulate_mouse_move(
point(px(200.), px(100.)),
None::<MouseButton>,
Modifiers::none(),
);
*disabled.borrow_mut() = false;
flush_frame(cx);
let reenabled = state_of(&recorded, "alpha");
assert!(
!reenabled.is_disabled && !reenabled.is_hovered && !reenabled.is_pressed,
"re-enabling a tag must not restore interaction recorded before it was disabled"
);
}
#[gpui::test]
fn inert_tag_group_tag_content_never_reports_hover_or_press(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
TagGroup::new("rp-tg-inert-state", vec![Tag::new("alpha", "Alpha")])
.selection_mode(SelectionMode::None)
.tag_content(move |tag, state| {
record_interactive(&record, tag.key(), state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let centre = point(px(28.), px(14.));
cx.simulate_mouse_move(centre, None::<MouseButton>, Modifiers::none());
cx.simulate_mouse_down(centre, MouseButton::Left, Modifiers::none());
flush_frame(cx);
let alpha = state_of(&recorded, "alpha");
assert!(
!alpha.is_hovered && !alpha.is_pressed,
"a tag without selection or an item action must stay pointer-inert"
);
}
#[gpui::test]
fn tag_group_tag_content_tracks_hover_press_and_selection(cx: &mut TestAppContext) {
let held = Rc::new(RefCell::new(HashSet::<SharedString>::new()));
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let cx = open_host(cx, move || {
let held = held.clone();
let record = record.clone();
let selected_now = held.borrow().clone();
gpui::div()
.flex()
.flex_col()
.child(
TagGroup::new(
"rp-tg-state",
vec![Tag::new("alpha", "Alpha"), Tag::new("beta", "Beta")],
)
.selection_mode(SelectionMode::Single)
.selected_keys(selected_now)
.tag_content(move |tag, state| {
record_interactive(&record, tag.key(), state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.on_selection_change(move |keys, window, _| {
*held.borrow_mut() = keys.clone();
window.refresh();
}),
)
.child(Button::new("after-rp-tag-group").label("After"))
.into_any_element()
});
cx.update(|window, _| window.activate_window());
cx.update(|_, cx| herogpui_components::extend::set_focus_visible(true, cx));
flush_frame(cx);
cx.simulate_mouse_move(
point(px(28.), px(14.)),
None::<MouseButton>,
Modifiers::none(),
);
flush_frame(cx);
assert!(
state_of(&recorded, "alpha").is_hovered,
"the frame after the move must see the hover on the first chip"
);
assert!(
!state_of(&recorded, "beta").is_hovered,
"a hover on one chip must not smear onto its neighbour"
);
cx.simulate_mouse_down(
point(px(28.), px(14.)),
MouseButton::Left,
Modifiers::none(),
);
flush_frame(cx);
let alpha = state_of(&recorded, "alpha");
assert!(
alpha.is_hovered && alpha.is_pressed,
"the frame after the down must see the press with the hover"
);
cx.simulate_mouse_up(
point(px(28.), px(14.)),
MouseButton::Left,
Modifiers::none(),
);
flush_frame(cx);
let alpha = state_of(&recorded, "alpha");
assert!(
alpha.is_hovered && !alpha.is_pressed && alpha.is_selected,
"the up must lift the press and select the chip"
);
cx.simulate_mouse_move(
point(px(90.), px(14.)),
None::<MouseButton>,
Modifiers::none(),
);
flush_frame(cx);
assert!(
!state_of(&recorded, "alpha").is_hovered && state_of(&recorded, "beta").is_hovered,
"the hover must follow the pointer between chips"
);
click(cx, 90., 14.);
flush_frame(cx);
assert!(
state_of(&recorded, "beta").is_selected,
"the clicked chip must report selected"
);
assert!(
!state_of(&recorded, "alpha").is_selected,
"the single selection must have moved off the first chip"
);
let beta = state_of(&recorded, "beta");
assert!(
beta.is_focused && !beta.is_focus_visible,
"the click's pointer seat must focus the clicked chip without a focus-visible ring"
);
assert!(
!state_of(&recorded, "alpha").is_focused,
"the pointer seat must carry the cursor off the first chip"
);
press(cx, "tab");
flush_frame(cx);
assert!(
["alpha", "beta"].iter().all(|key| {
let state = state_of(&recorded, key);
!state.is_focused && !state.is_focus_visible
}),
"moving focus out of the TagGroup must clear every render-prop focus and ring state"
);
press(cx, "tab");
flush_frame(cx);
let beta = state_of(&recorded, "beta");
assert!(
beta.is_focused && beta.is_focus_visible,
"keyboard re-entry must ring the TagGroup's roving stop"
);
assert!(
!state_of(&recorded, "alpha").is_focused,
"keyboard re-entry must land on the seated cursor's chip"
);
}
#[gpui::test]
fn tag_group_without_content_needs_no_slot(cx: &mut TestAppContext) {
let selected = events();
let selections = selected.clone();
let removed = events();
let removals = removed.clone();
let held = Rc::new(RefCell::new(HashSet::<SharedString>::new()));
let cx = open_host(cx, move || {
let selected = selected.clone();
let removed = removed.clone();
let held = held.clone();
let selected_now = held.borrow().clone();
TagGroup::new(
"rp-tg-plain",
vec![Tag::new("alpha", "Alpha"), Tag::new("beta", "Beta")],
)
.selection_mode(SelectionMode::Single)
.selected_keys(selected_now)
.on_selection_change(move |keys, window, _| {
selected.borrow_mut().push(sorted_join(keys));
*held.borrow_mut() = keys.clone();
window.refresh();
})
.on_remove(move |keys, _, _| removed.borrow_mut().push(sorted_join(keys)))
.into_any_element()
});
press(cx, "tab");
press(cx, "enter");
assert_eq!(
selections.borrow().as_slice(),
["alpha"],
"the plain group must answer Enter like the closure twin"
);
press(cx, "enter");
assert_eq!(
selections.borrow().as_slice(),
["alpha", ""],
"Enter again must toggle the single selection off"
);
let w =
cx.update(|window, _| text_width(window.text_system(), "Alpha", 12.0, FontWeight::NORMAL));
click(cx, 8. + w / 2., 14.);
assert_eq!(
selections.borrow().as_slice(),
["alpha", "", "alpha"],
"the plain group must answer the pointer like the closure twin"
);
press(cx, "delete");
assert_eq!(
removals.borrow().as_slice(),
["alpha"],
"Delete must remove the focused tag of the plain group"
);
}
#[gpui::test]
fn radio_group_option_content_hands_v3_field_state(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(Vec::new()));
let record = recorded.clone();
let _cx = open_host(cx, move || {
let record = record.clone();
RadioGroup::new("rp-rg-field-state", vec!["One".into()])
.is_disabled(true)
.is_invalid(true)
.is_required(true)
.is_read_only(true)
.option_content(move |_, state| {
record.borrow_mut().push((
state.is_selected,
state.is_disabled,
state.is_read_only,
state.is_invalid,
state.is_required,
));
gpui::div().into_any_element()
})
.into_any_element()
});
assert_eq!(
recorded.borrow().as_slice(),
[(false, true, true, true, true)],
"the Radio root render prop must receive every pinned field-level state"
);
}
#[gpui::test]
fn radio_group_option_content_keeps_per_option_state_local(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let _cx = open_host(cx, move || {
let record = record.clone();
RadioGroup::new(
"rp-rg-local-field-state",
vec![
RadioOption::new("One")
.is_disabled(true)
.error_message("Unavailable"),
RadioOption::new("Two"),
],
)
.option_content(move |label, state| {
record_radio_state(&record, label, state);
gpui::div().into_any_element()
})
.into_any_element()
});
let one = radio_state_of(&recorded, "One");
let two = radio_state_of(&recorded, "Two");
assert!(one.is_disabled && one.is_invalid);
assert!(!two.is_disabled && !two.is_invalid);
}
#[gpui::test]
fn radio_group_option_content_renders_at_all(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let _cx = open_host(cx, move || {
let record = record.clone();
RadioGroup::new(
"rp-rg-render",
vec!["One".into(), "Two".into(), "Three".into()],
)
.option_content(move |label, state| {
record_radio_state(&record, label, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let one = radio_state_of(&recorded, "One");
assert!(
!one.is_selected
&& !one.is_disabled
&& !one.is_read_only
&& !one.is_invalid
&& !one.is_required,
"the first option must receive an idle field state"
);
for label in ["Two", "Three"] {
let state = radio_state_of(&recorded, label);
assert!(
!state.is_selected
&& !state.is_disabled
&& !state.is_read_only
&& !state.is_invalid
&& !state.is_required,
"{label} must receive an idle field state"
);
}
}
#[gpui::test]
fn radio_group_option_content_tracks_selection(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let picks = events();
let picked = picks.clone();
let record = recorded.clone();
let cx = open_host(cx, move || {
let record = record.clone();
let picks = picks.clone();
RadioGroup::new(
"rp-rg-state",
vec!["One".into(), "Two".into(), "Three".into()],
)
.default_value("One")
.option_content(move |label, state| {
record_radio_state(&record, label, state);
gpui::div().w(px(40.)).h(px(20.)).into_any_element()
})
.on_change(move |value, _, _| picks.borrow_mut().push(value.to_string()))
.into_any_element()
});
assert!(
radio_state_of(&recorded, "One").is_selected,
"the default selection must reach the closure"
);
click(cx, 48., 46.);
flush_frame(cx);
assert!(
radio_state_of(&recorded, "Two").is_selected,
"the clicked option must report selected"
);
assert!(
!radio_state_of(&recorded, "One").is_selected,
"the earlier selection must be replaced"
);
assert_eq!(picked.borrow().as_slice(), ["Two"]);
press(cx, "tab");
press(cx, "down");
flush_frame(cx);
let three = radio_state_of(&recorded, "Three");
assert!(three.is_selected, "the arrow must select the third option");
assert_eq!(picked.borrow().as_slice(), ["Two", "Three"]);
}
#[gpui::test]
fn calendar_cell_renders_at_all(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let _cx = open_host(cx, move || {
let record = record.clone();
Calendar::new(state_for_view.clone())
.default_value(Date::new(2026, 8, 15))
.first_day_of_week(Weekday::Mon)
.cell(move |state| {
record_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let selected = cell_of(&recorded, "2026-08-15");
assert_eq!(selected.formatted_date, "15");
assert!(selected.is_selected && !selected.is_outside_month && !selected.is_disabled);
let constraints = DateConstraints {
first_day_of_week: Weekday::Mon,
..Default::default()
};
let lead = constraints.lead_cells(2026, 8);
assert_eq!(lead, 5, "the month's own derivation must give a lead");
let rows = constraints.rows(2026, 8);
let cells = recorded.borrow();
let in_month = cells.values().filter(|cell| !cell.is_outside_month).count();
assert_eq!(
in_month, 31,
"every in-month day must render with the closure"
);
let spills = cells.values().filter(|cell| cell.is_outside_month).count();
assert_eq!(
spills,
rows * 7 - 31,
"both adjacent months' cells must reach the render closure"
);
for cell in cells.values() {
if cell.is_outside_month {
assert!(cell.is_disabled, "an outside-month cell must be disabled");
assert!(
!cell.is_unavailable,
"the unconfigured unavailable predicate is false"
);
assert!(
!cell.is_selected,
"an outside-month copy cannot report selected"
);
} else {
assert!(
!cell.is_disabled,
"an unconstrained in-month cell must not report disabled"
);
}
}
}
#[gpui::test]
fn calendar_cell_spill_dates_preserve_their_identity(cx: &mut TestAppContext) {
let invocations = Rc::new(RefCell::new(Vec::<(String, String, bool)>::new()));
let record = invocations.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let _cx = open_host(cx, move || {
let record = record.clone();
Calendar::new(state_for_view.clone())
.default_value(Date::new(2026, 8, 15))
.first_day_of_week(Weekday::Mon)
.cell(move |state| {
record.borrow_mut().push((
state.date.format_iso(),
state.formatted_date.to_string(),
state.is_outside_month,
));
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let spills: HashMap<String, String> = invocations
.borrow()
.iter()
.filter(|(_, _, outside)| *outside)
.map(|(date, label, _)| (date.clone(), label.clone()))
.collect();
assert_eq!(
spills,
HashMap::from([
("2026-07-27".into(), "27".into()),
("2026-07-28".into(), "28".into()),
("2026-07-29".into(), "29".into()),
("2026-07-30".into(), "30".into()),
("2026-07-31".into(), "31".into()),
("2026-09-01".into(), "1".into()),
("2026-09-02".into(), "2".into()),
("2026-09-03".into(), "3".into()),
("2026-09-04".into(), "4".into()),
("2026-09-05".into(), "5".into()),
("2026-09-06".into(), "6".into()),
]),
"both spill edges must hand the closure their real dates and labels"
);
}
#[gpui::test]
fn calendar_outside_cells_keep_date_derived_unavailable_state(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let _cx = open_host(cx, move || {
let record = record.clone();
Calendar::new(state_for_view.clone())
.default_value(Date::new(2026, 8, 15))
.is_date_unavailable(|date| {
date == Date::new(2026, 7, 31) || date == Date::new(2026, 9, 1)
})
.cell(move |state| {
record_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
for date in ["2026-07-31", "2026-09-01"] {
let cell = cell_of(&recorded, date);
assert!(
cell.is_outside_month && cell.is_disabled && cell.is_unavailable && !cell.is_selected,
"outside cell {date} must retain its predicate-derived unavailable state"
);
}
}
#[gpui::test]
fn calendar_cell_distinguishes_read_only_unavailable_and_disabled(cx: &mut TestAppContext) {
let focuses = events();
let focused = focuses.clone();
let changes = events();
let changed = changes.clone();
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let cx = open_host(cx, move || {
let focuses = focuses.clone();
let changes = changes.clone();
let record = record.clone();
Calendar::new(state_for_view.clone())
.default_value(Date::new(2026, 8, 15))
.is_read_only(true)
.is_date_unavailable(|date| date == Date::new(2026, 8, 16))
.on_focus_change(move |date, _, _| {
focuses.borrow_mut().push(date.format_iso());
})
.on_change(move |date, _, _| {
changes
.borrow_mut()
.push(date.map_or_else(|| "none".into(), |date| date.format_iso()));
})
.cell(move |state| {
record_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let selected = cell_of(&recorded, "2026-08-15");
assert!(selected.is_selected && !selected.is_unavailable && !selected.is_disabled);
let unavailable = cell_of(&recorded, "2026-08-16");
assert!(unavailable.is_unavailable && !unavailable.is_selected && !unavailable.is_disabled);
press(cx, "tab");
press(cx, "right");
let (x17, y17) = cal_day(2026, 8, 17);
click(cx, x17, y17);
let (x16, y16) = cal_day(2026, 8, 16);
click(cx, x16, y16);
assert_eq!(
focused.borrow().as_slice(),
["2026-08-16"],
"read-only and unavailable cells remain keyboard-focusable but must be pointer-inert"
);
assert!(
changed.borrow().is_empty(),
"read-only cells must never report a selection change"
);
}
#[gpui::test]
fn calendar_cell_suppresses_selection_for_unavailable_and_disabled_values(cx: &mut TestAppContext) {
let disabled_cells = Rc::new(RefCell::new(HashMap::new()));
let disabled_record = disabled_cells.clone();
let unavailable_cells = Rc::new(RefCell::new(HashMap::new()));
let unavailable_record = unavailable_cells.clone();
let disabled_state = cx.new(|cx| CalendarState::new(cx));
let unavailable_state = cx.new(|cx| CalendarState::new(cx));
let _cx = open_host(cx, move || {
let disabled_record = disabled_record.clone();
let unavailable_record = unavailable_record.clone();
gpui::div()
.child(
Calendar::new(disabled_state.clone())
.default_value(Date::new(2026, 8, 14))
.min_value(Date::new(2026, 8, 15))
.cell(move |state| {
record_cell(&disabled_record, state);
gpui::div().into_any_element()
}),
)
.child(
Calendar::new(unavailable_state.clone())
.default_value(Date::new(2026, 8, 16))
.is_date_unavailable(|date| date == Date::new(2026, 8, 16))
.cell(move |state| {
record_cell(&unavailable_record, state);
gpui::div().into_any_element()
}),
)
.into_any_element()
});
let disabled = cell_of(&disabled_cells, "2026-08-14");
assert!(disabled.is_disabled && !disabled.is_selected);
let unavailable = cell_of(&unavailable_cells, "2026-08-16");
assert!(unavailable.is_unavailable && !unavailable.is_disabled && !unavailable.is_selected);
}
#[gpui::test]
fn calendar_multiple_deselection_keeps_pointer_focus(cx: &mut TestAppContext) {
let changes = events();
let changed = changes.clone();
let focuses = events();
let focused = focuses.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let cx = open_host(cx, move || {
let changes = changes.clone();
let focuses = focuses.clone();
Calendar::new(state_for_view.clone())
.selection_mode(SelectionMode::Multiple)
.default_values([Date::new(2026, 8, 15), Date::new(2026, 8, 16)])
.on_focus_change(move |date, _, _| {
focuses.borrow_mut().push(date.format_iso());
})
.on_change_all(move |dates, _, _| {
changes.borrow_mut().push(
dates
.iter()
.map(Date::format_iso)
.collect::<Vec<_>>()
.join(","),
);
})
.into_any_element()
});
let (x16, y16) = cal_day(2026, 8, 16);
click(cx, x16, y16);
press(cx, "enter");
assert_eq!(focused.borrow().as_slice(), ["2026-08-16"]);
assert_eq!(
changed.borrow().as_slice(),
["2026-08-15", "2026-08-15,2026-08-16"],
"Enter after deselection must toggle the pointer-focused date back on"
);
}
#[gpui::test]
fn calendar_outside_selected_copy_is_unselected_and_inert(cx: &mut TestAppContext) {
let changes = events();
let changed = changes.clone();
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| CalendarState::new(cx));
state.update(cx, |state, _| {
state.toggle(Date::new(2026, 9, 1), SelectionMode::Single);
state.set_anchor(Date::new(2026, 8, 15));
});
let state_for_view = state.clone();
let cx = open_host(cx, move || {
let changes = changes.clone();
let record = record.clone();
Calendar::new(state_for_view.clone())
.first_day_of_week(Weekday::Mon)
.on_change(move |date, _, _| {
changes
.borrow_mut()
.push(date.map_or_else(|| "none".into(), |date| date.format_iso()));
})
.cell(move |state| {
record_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let outside = cell_of(&recorded, "2026-09-01");
assert!(outside.is_outside_month && outside.is_disabled && !outside.is_selected);
click(cx, cal_col_x(0), cal_row_y(0));
click(cx, cal_col_x(1), cal_row_y(5));
assert!(
changed.borrow().is_empty(),
"neither outside-month edge may report a selection"
);
assert_eq!(
cx.update(|_, cx| state.read(cx).selected()),
Some(Date::new(2026, 9, 1)),
"outside presses must preserve the selected adjacent-month date"
);
}
#[gpui::test]
fn calendar_multimonth_boundary_cells_keep_independent_state(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(Vec::new()));
let record = recorded.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let _cx = open_host(cx, move || {
let record = record.clone();
Calendar::new(state_for_view.clone())
.default_value(Date::new(2026, 8, 15))
.visible_duration(VisibleDuration::Months(2))
.cell(move |state| {
if matches!(
state.date.format_iso().as_str(),
"2026-08-31" | "2026-09-01"
) {
record
.borrow_mut()
.push((state.date.format_iso(), state.is_outside_month));
}
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let mut boundary = recorded.borrow().clone();
boundary.sort();
assert_eq!(
boundary,
[
("2026-08-31".into(), false),
("2026-08-31".into(), true),
("2026-09-01".into(), false),
("2026-09-01".into(), true),
],
"each grid must keep its in-month and outside-month copy independent"
);
}
#[gpui::test]
fn calendar_cell_tracks_selection(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| CalendarState::new(cx));
let state_for_view = state;
let cx = open_host(cx, move || {
let record = record.clone();
Calendar::new(state_for_view.clone())
.default_value(Date::new(2026, 8, 15))
.cell(move |state| {
record_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
assert!(
cell_of(&recorded, "2026-08-15").is_selected,
"the seeded pick must reach the closure"
);
let (x, y) = cal_day(2026, 8, 16);
click(cx, x, y);
flush_frame(cx);
assert!(
cell_of(&recorded, "2026-08-16").is_selected,
"the picked day must report selected"
);
assert!(
!cell_of(&recorded, "2026-08-15").is_selected,
"the single selection must have left the seeded day"
);
}
#[gpui::test]
fn range_calendar_cell_renders_at_all(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |s, _| {
s.view_year = 2026;
s.view_month = 8;
s.view_day = 1;
s.user_navigated = true;
});
let state_for_view = state;
let _cx = open_host(cx, move || {
let record = record.clone();
RangeCalendar::new(state_for_view.clone())
.cell(move |state| {
record_range_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let five = range_cell_of(&recorded, "2026-08-05");
assert!(
!five.is_selected && !five.is_selection_start && !five.is_selection_end,
"every cell must record the idle range state on the first frame"
);
assert!(
!five.is_disabled && !five.is_outside_month,
"an in-month day of an enabled calendar must report neither"
);
let cells = recorded.borrow();
assert_eq!(
cells.len(),
42,
"the six-week grid must expose 42 real dates"
);
assert_eq!(
cells.values().filter(|cell| !cell.is_outside_month).count(),
31,
"all August dates must remain in-month"
);
assert_eq!(
cells.values().filter(|cell| cell.is_outside_month).count(),
11,
"the five July and six September copies must reach the closure"
);
for cell in cells.values().filter(|cell| cell.is_outside_month) {
assert!(cell.is_disabled && !cell.is_selected);
assert!(!cell.is_selection_start && !cell.is_selection_end);
}
}
#[gpui::test]
fn range_calendar_spill_dates_preserve_their_identity(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |state, _| {
state.view_year = 2026;
state.view_month = 8;
state.view_day = 1;
state.user_navigated = true;
});
let _cx = open_host(cx, move || {
let record = record.clone();
RangeCalendar::new(state.clone())
.first_day_of_week(Weekday::Mon)
.cell(move |cell| {
record_range_cell(&record, cell);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let spills: HashMap<String, String> = recorded
.borrow()
.values()
.filter(|cell| cell.is_outside_month)
.map(|cell| (cell.date.format_iso(), cell.formatted_date.to_string()))
.collect();
assert_eq!(
spills,
HashMap::from([
("2026-07-27".into(), "27".into()),
("2026-07-28".into(), "28".into()),
("2026-07-29".into(), "29".into()),
("2026-07-30".into(), "30".into()),
("2026-07-31".into(), "31".into()),
("2026-09-01".into(), "1".into()),
("2026-09-02".into(), "2".into()),
("2026-09-03".into(), "3".into()),
("2026-09-04".into(), "4".into()),
("2026-09-05".into(), "5".into()),
("2026-09-06".into(), "6".into()),
])
);
}
#[gpui::test]
fn range_calendar_weeks_in_month_keeps_exact_row_count(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |state, _| {
state.view_year = 2026;
state.view_month = 8;
state.view_day = 1;
state.user_navigated = true;
});
let _cx = open_host(cx, move || {
let record = record.clone();
RangeCalendar::new(state.clone())
.first_day_of_week(Weekday::Mon)
.weeks_in_month(7)
.cell(move |cell| {
record_range_cell(&record, cell);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
assert_eq!(recorded.borrow().len(), 49);
let final_cell = range_cell_of(&recorded, "2026-09-13");
assert!(final_cell.is_outside_month && final_cell.is_disabled);
}
#[gpui::test]
fn range_calendar_outside_cells_keep_date_derived_state(cx: &mut TestAppContext) {
let changes = events();
let changed = changes.clone();
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |state, _| {
state.view_year = 2026;
state.view_month = 8;
state.view_day = 1;
state.start = Some(Date::new(2026, 9, 1));
state.end = Some(Date::new(2026, 9, 2));
state.user_navigated = true;
});
let cx = open_host(cx, move || {
let changes = changes.clone();
let record = record.clone();
RangeCalendar::new(state.clone())
.is_date_unavailable(|date, _| date == Date::new(2026, 7, 31))
.on_change(move |start, end, _, _| {
changes
.borrow_mut()
.push(format!("{}..{}", start.format_iso(), end.format_iso()));
})
.cell(move |cell| {
record_range_cell(&record, cell);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let july = range_cell_of(&recorded, "2026-07-31");
assert!(july.is_outside_month && july.is_disabled && july.is_unavailable);
assert!(!july.is_selected && !july.is_selection_start && !july.is_selection_end);
let september = range_cell_of(&recorded, "2026-09-01");
assert!(september.is_outside_month && september.is_disabled && !september.is_selected);
assert!(
september.is_selection_start,
"RAC preserves endpoint identity even though the outside copy is not selected"
);
click(cx, range_col_x(0), range_row_y(0));
click(cx, range_col_x(1), range_row_y(5));
assert!(
changed.borrow().is_empty(),
"neither outside-month edge may report a range change"
);
}
#[gpui::test]
fn range_calendar_read_only_and_unavailable_states_stay_independent(cx: &mut TestAppContext) {
let changes = events();
let changed = changes.clone();
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |state, _| {
state.view_year = 2026;
state.view_month = 8;
state.view_day = 1;
state.start = Some(Date::new(2026, 8, 15));
state.end = Some(Date::new(2026, 8, 17));
state.user_navigated = true;
});
let cx = open_host(cx, move || {
let changes = changes.clone();
let record = record.clone();
RangeCalendar::new(state.clone())
.is_read_only(true)
.is_date_unavailable(|date, _| date == Date::new(2026, 8, 16))
.on_change(move |_, _, _, _| changes.borrow_mut().push("changed".into()))
.cell(move |cell| {
record_range_cell(&record, cell);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let start = range_cell_of(&recorded, "2026-08-15");
assert!(start.is_selected && start.is_selection_start && !start.is_disabled);
let unavailable = range_cell_of(&recorded, "2026-08-16");
assert!(unavailable.is_unavailable && !unavailable.is_disabled && !unavailable.is_selected);
let (start_x, start_y) = range_day(2026, 8, 15);
click(cx, start_x, start_y);
let (unavailable_x, unavailable_y) = range_day(2026, 8, 16);
click(cx, unavailable_x, unavailable_y);
assert!(changed.borrow().is_empty());
}
#[gpui::test]
fn range_calendar_multimonth_boundary_copies_keep_independent_state(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(Vec::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |state, _| {
state.view_year = 2026;
state.view_month = 8;
state.view_day = 1;
state.start = Some(Date::new(2026, 8, 31));
state.end = Some(Date::new(2026, 9, 2));
state.user_navigated = true;
});
let _cx = open_host(cx, move || {
let record = record.clone();
RangeCalendar::new(state.clone())
.visible_duration(VisibleDuration::Months(2))
.cell(move |cell| {
if matches!(cell.date.format_iso().as_str(), "2026-08-31" | "2026-09-01") {
record.borrow_mut().push((
cell.date.format_iso(),
cell.is_outside_month,
cell.is_selected,
cell.is_selection_start,
));
}
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let mut boundary = recorded.borrow().clone();
boundary.sort();
assert_eq!(
boundary,
[
("2026-08-31".into(), false, true, true),
("2026-08-31".into(), true, false, true),
("2026-09-01".into(), false, true, false),
("2026-09-01".into(), true, false, false),
]
);
}
#[gpui::test]
fn range_calendar_pointer_end_hands_focus_to_keyboard(cx: &mut TestAppContext) {
let changes = Rc::new(RefCell::new(Vec::new()));
let changed = changes.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |state, _| {
state.view_year = 2026;
state.view_month = 8;
state.view_day = 1;
state.user_navigated = true;
});
let cx = open_host(cx, move || {
let changes = changes.clone();
RangeCalendar::new(state.clone())
.on_change(move |start, end, _, _| {
changes.borrow_mut().push((*start, *end));
})
.into_any_element()
});
let (five_x, five_y) = range_day(2026, 8, 5);
click(cx, five_x, five_y);
let (eight_x, eight_y) = range_day(2026, 8, 8);
click(cx, eight_x, eight_y);
press(cx, "enter");
assert_eq!(
changed.borrow().as_slice(),
[(Date::new(2026, 8, 5), Date::new(2026, 8, 8))]
);
}
#[gpui::test]
fn range_calendar_cell_tracks_anchor_preview_and_range(cx: &mut TestAppContext) {
let recorded = Rc::new(RefCell::new(HashMap::new()));
let record = recorded.clone();
let state = cx.new(|cx| DateRangeState::new(cx));
state.update(cx, |s, _| {
s.view_year = 2026;
s.view_month = 8;
s.view_day = 1;
s.user_navigated = true;
});
let state_for_view = state;
let cx = open_host(cx, move || {
let record = record.clone();
RangeCalendar::new(state_for_view.clone())
.cell(move |state| {
record_range_cell(&record, state);
gpui::div().w(px(20.)).h(px(20.)).into_any_element()
})
.into_any_element()
});
let (five_x, five_y) = range_day(2026, 8, 5);
click(cx, five_x, five_y);
flush_frame(cx);
let five = range_cell_of(&recorded, "2026-08-05");
assert!(
five.is_selection_start && five.is_selected,
"the anchor cell must report the open-ended start"
);
assert!(
five.is_selection_end,
"the pointer over the anchor previews a one-day range"
);
let (eight_x, eight_y) = range_day(2026, 8, 8);
cx.simulate_mouse_move(
point(px(eight_x), px(eight_y)),
None::<MouseButton>,
Modifiers::none(),
);
flush_frame(cx);
let eight = range_cell_of(&recorded, "2026-08-08");
assert!(
eight.is_selection_end && eight.is_selected,
"the hovered day must report the preview end"
);
assert!(
range_cell_of(&recorded, "2026-08-07").is_selected,
"a day between the anchor and the preview must report selected"
);
assert!(
!range_cell_of(&recorded, "2026-08-10").is_selected,
"a day past the preview must stay unselected"
);
let (twelve_x, twelve_y) = range_day(2026, 8, 12);
click(cx, twelve_x, twelve_y);
flush_frame(cx);
let twelve = range_cell_of(&recorded, "2026-08-12");
assert!(
twelve.is_selection_end && twelve.is_selected,
"the picked end must report the closed range"
);
assert!(
range_cell_of(&recorded, "2026-08-05").is_selection_start,
"the anchor must keep its start"
);
assert!(
range_cell_of(&recorded, "2026-08-10").is_selected,
"a day inside the range must report selected"
);
assert!(
!range_cell_of(&recorded, "2026-08-14").is_selected,
"a day outside the range must stay unselected"
);
}