Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/config/action_meta/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,18 @@ fn action_meta_covers_surface_actions() {
meta.in_command_palette
});
}

#[test]
fn action_display_label_strips_toggle_prefix() {
assert_eq!(action_display_label(Action::ToggleStatusBar), "Status Bar");
}

#[test]
fn action_display_label_strips_mode_suffix() {
assert_eq!(action_display_label(Action::ToggleWhiteboard), "Whiteboard");
}

#[test]
fn action_display_label_uses_short_label_for_ellipse_tool() {
assert_eq!(action_display_label(Action::SelectEllipseTool), "Circle");
}
153 changes: 115 additions & 38 deletions src/config/keybindings/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,36 @@ fn test_parse_with_spaces() {
assert!(binding.shift);
}

#[test]
fn test_parse_plus_key() {
let binding = KeyBinding::parse("Ctrl+Shift++").unwrap();
assert_eq!(binding.key, "+");
assert!(binding.ctrl);
assert!(binding.shift);
assert!(!binding.alt);
}

#[test]
fn test_parse_control_alias() {
let binding = KeyBinding::parse("Control+Alt+Delete").unwrap();
assert_eq!(binding.key, "Delete");
assert!(binding.ctrl);
assert!(binding.alt);
assert!(!binding.shift);
}

#[test]
fn test_parse_requires_non_modifier_key() {
let err = KeyBinding::parse("Ctrl+Shift").unwrap_err();
assert!(err.contains("No key specified"));
}

#[test]
fn test_display_normalizes_modifier_order() {
let binding = KeyBinding::parse("Shift+Ctrl+W").unwrap();
assert_eq!(binding.to_string(), "Ctrl+Shift+W");
}

#[test]
fn test_matches() {
let binding = KeyBinding::parse("Ctrl+Shift+W").unwrap();
Expand Down Expand Up @@ -91,53 +121,33 @@ fn test_parse_modifier_order_independence() {

#[test]
fn test_build_action_map() {
let config = KeybindingsConfig::default();
let mut config = KeybindingsConfig::default();
config.core.exit = vec!["Ctrl+Alt+Shift+1".to_string()];
config.core.undo = vec!["Ctrl+Alt+Shift+2".to_string()];
config.core.redo = vec!["Ctrl+Alt+Shift+3".to_string()];
config.ui.toggle_help = vec!["Ctrl+Alt+Shift+4".to_string()];
config.board.toggle_whiteboard = vec!["Ctrl+Alt+Shift+5".to_string()];
let map = config.build_action_map().unwrap();

// Check that some default bindings are present
let escape = KeyBinding::parse("Escape").unwrap();
assert_eq!(map.get(&escape), Some(&Action::Exit));

let ctrl_z = KeyBinding::parse("Ctrl+Z").unwrap();
assert_eq!(map.get(&ctrl_z), Some(&Action::Undo));

let ctrl_shift_z = KeyBinding::parse("Ctrl+Shift+Z").unwrap();
assert_eq!(map.get(&ctrl_shift_z), Some(&Action::Redo));

let move_front = KeyBinding::parse("]").unwrap();
assert_eq!(map.get(&move_front), Some(&Action::MoveSelectionToFront));

let move_back = KeyBinding::parse("[").unwrap();
assert_eq!(map.get(&move_back), Some(&Action::MoveSelectionToBack));

let copy_selection = KeyBinding::parse("Ctrl+Alt+C").unwrap();
assert_eq!(map.get(&copy_selection), Some(&Action::CopySelection));

let capture_selection = KeyBinding::parse("Ctrl+Shift+C").unwrap();
assert_eq!(
map.get(&capture_selection),
Some(&Action::CaptureClipboardSelection)
map.get(&KeyBinding::parse("Ctrl+Alt+Shift+1").unwrap()),
Some(&Action::Exit)
);

let select_all = KeyBinding::parse("Ctrl+A").unwrap();
assert_eq!(map.get(&select_all), Some(&Action::SelectAll));

let toggle_highlight = KeyBinding::parse("Ctrl+Shift+H").unwrap();
assert_eq!(
map.get(&toggle_highlight),
Some(&Action::ToggleClickHighlight)
map.get(&KeyBinding::parse("Ctrl+Alt+Shift+2").unwrap()),
Some(&Action::Undo)
);

let toggle_highlight_tool = KeyBinding::parse("Ctrl+Alt+H").unwrap();
assert_eq!(
map.get(&toggle_highlight_tool),
Some(&Action::ToggleHighlightTool)
map.get(&KeyBinding::parse("Ctrl+Alt+Shift+3").unwrap()),
Some(&Action::Redo)
);
assert_eq!(
map.get(&KeyBinding::parse("Ctrl+Alt+Shift+4").unwrap()),
Some(&Action::ToggleHelp)
);

let reset_arrow_labels = KeyBinding::parse("Ctrl+Shift+R").unwrap();
assert_eq!(
map.get(&reset_arrow_labels),
Some(&Action::ResetArrowLabelCounter)
map.get(&KeyBinding::parse("Ctrl+Alt+Shift+5").unwrap()),
Some(&Action::ToggleWhiteboard)
);
}

Expand Down Expand Up @@ -170,3 +180,70 @@ fn test_duplicate_with_different_modifier_order() {
assert!(err_msg.contains("Duplicate keybinding"));
assert!(err_msg.contains("Shift+Ctrl+W"));
}

#[test]
fn test_parse_plus_key_without_modifiers() {
let binding = KeyBinding::parse("+").unwrap();
assert_eq!(binding.key, "+");
assert!(!binding.ctrl);
assert!(!binding.shift);
assert!(!binding.alt);
}

#[test]
fn test_parse_trims_surrounding_whitespace() {
let binding = KeyBinding::parse(" Escape ").unwrap();
assert_eq!(binding.key, "Escape");
assert!(!binding.ctrl);
assert!(!binding.shift);
assert!(!binding.alt);
}

#[test]
fn test_matches_requires_exact_alt_state() {
let binding = KeyBinding::parse("Alt+X").unwrap();
assert!(binding.matches("x", false, false, true));
assert!(!binding.matches("x", false, false, false));
}

#[test]
fn test_build_action_bindings_preserves_declared_binding_order() {
let mut config = KeybindingsConfig::default();
config.ui.toggle_help = vec![
"Ctrl+Alt+Shift+1".to_string(),
"Ctrl+Alt+Shift+2".to_string(),
];
config.core.redo = vec![
"Ctrl+Alt+Shift+3".to_string(),
"Ctrl+Alt+Shift+4".to_string(),
];
let bindings = config.build_action_bindings().unwrap();

assert_eq!(
bindings.get(&Action::ToggleHelp),
Some(&vec![
KeyBinding::parse("Ctrl+Alt+Shift+1").unwrap(),
KeyBinding::parse("Ctrl+Alt+Shift+2").unwrap(),
])
);
assert_eq!(
bindings.get(&Action::Redo),
Some(&vec![
KeyBinding::parse("Ctrl+Alt+Shift+3").unwrap(),
KeyBinding::parse("Ctrl+Alt+Shift+4").unwrap(),
])
);
}

#[test]
fn test_build_action_bindings_reports_duplicate_keybindings() {
let mut config = KeybindingsConfig::default();
config.core.exit = vec!["Ctrl+Z".to_string()];
config.core.undo = vec!["Ctrl+Z".to_string()];

let result = config.build_action_bindings();
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(err_msg.contains("Duplicate keybinding"));
assert!(err_msg.contains("Ctrl+Z"));
}
39 changes: 39 additions & 0 deletions src/draw/render/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,42 @@ pub fn selection_handle_rects(bounds: &Rect) -> [Rect; 8] {
}),
]
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn selection_handle_rects_cover_all_corners() {
let bounds = Rect::new(10, 20, 30, 40).unwrap();
let handles = selection_handle_rects(&bounds);

assert!(handles[0].contains(10, 20));
assert!(handles[1].contains(40, 20));
assert!(handles[2].contains(10, 60));
assert!(handles[3].contains(40, 60));
}

#[test]
fn selection_handle_rects_cover_edge_midpoints() {
let bounds = Rect::new(10, 20, 30, 40).unwrap();
let handles = selection_handle_rects(&bounds);

assert!(handles[4].contains(25, 20));
assert!(handles[5].contains(25, 60));
assert!(handles[6].contains(10, 40));
assert!(handles[7].contains(40, 40));
}

#[test]
fn selection_handle_rects_remain_valid_for_tiny_bounds() {
let bounds = Rect::new(0, 0, 1, 1).unwrap();
let handles = selection_handle_rects(&bounds);

assert!(
handles
.iter()
.all(|handle| handle.width > 0 && handle.height > 0)
);
}
}
38 changes: 38 additions & 0 deletions src/draw/shape/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,41 @@ pub(crate) fn ensure_positive_rect_f64(
let max_y = max_y.ceil() as i32;
ensure_positive_rect(min_x, min_y, max_x, max_y)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn bounding_box_for_points_returns_none_for_empty_input() {
assert_eq!(bounding_box_for_points(&[], 2.0), None);
}

#[test]
fn bounding_box_for_rect_handles_negative_drag_dimensions() {
assert_eq!(
bounding_box_for_rect(10, 20, -4, -6, 1.0),
Rect::new(5, 13, 6, 8)
);
}

#[test]
fn ensure_positive_rect_makes_degenerate_integer_bounds_visible() {
assert_eq!(ensure_positive_rect(5, 7, 5, 7), Rect::new(5, 7, 1, 1));
}

#[test]
fn ensure_positive_rect_f64_uses_floor_and_ceil_bounds() {
assert_eq!(
ensure_positive_rect_f64(1.2, 3.8, 4.1, 6.0),
Rect::new(1, 3, 4, 3)
);
}

#[test]
fn stroke_padding_never_drops_below_one_pixel() {
assert_eq!(stroke_padding(0.0), 1);
assert_eq!(stroke_padding(1.1), 1);
assert_eq!(stroke_padding(2.1), 2);
}
}
33 changes: 33 additions & 0 deletions src/draw/shape/step_marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,36 @@ pub(crate) fn step_marker_bounds(
let max_y = (y as f64 + total).ceil() as i32;
Rect::from_min_max(min_x, min_y, max_x, max_y)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step_marker_outline_thickness_has_minimum_floor() {
assert_eq!(step_marker_outline_thickness(1.0), 1.5);
assert_eq!(step_marker_outline_thickness(20.0), 2.4);
}

#[test]
fn step_marker_radius_grows_with_font_size() {
let font = FontDescriptor::default();
assert!(step_marker_radius(1, 32.0, &font) > step_marker_radius(1, 12.0, &font));
}

#[test]
fn step_marker_radius_grows_for_multi_digit_labels() {
let font = FontDescriptor::default();
assert!(step_marker_radius(88, 18.0, &font) >= step_marker_radius(8, 18.0, &font));
}

#[test]
fn step_marker_bounds_are_centered_around_marker_position() {
let font = FontDescriptor::default();
let bounds = step_marker_bounds(50, 75, 3, 18.0, &font).expect("step marker bounds");

assert!(bounds.contains(50, 75));
assert!(bounds.width > 0);
assert!(bounds.height > 0);
}
}
55 changes: 55 additions & 0 deletions src/draw/shape/text_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ pub fn invalidate_text_cache() {
mod tests {
use super::*;

fn measurement(width: f64) -> TextMeasurement {
TextMeasurement {
ink_x: 0.0,
ink_y: 0.0,
ink_width: width,
ink_height: 10.0,
baseline: 8.0,
}
}

#[test]
fn test_cache_returns_same_measurement() {
let text = "Hello World";
Expand Down Expand Up @@ -248,6 +258,51 @@ mod tests {
assert_eq!(m2.ink_height, m2_cached.ink_height);
}

#[test]
fn test_cache_evicts_oldest_entry_at_capacity() {
let mut cache = TextMeasurementCache::new(2);
let key_a = TextCacheKey::new("A", "Sans", 12.0, None);
let key_b = TextCacheKey::new("B", "Sans", 12.0, None);
let key_c = TextCacheKey::new("C", "Sans", 12.0, None);

cache.insert(key_a.clone(), measurement(10.0));
cache.insert(key_b.clone(), measurement(20.0));
cache.insert(key_c.clone(), measurement(30.0));

assert!(cache.get(&key_a).is_none());
assert_eq!(cache.get(&key_b).unwrap().ink_width, 20.0);
assert_eq!(cache.get(&key_c).unwrap().ink_width, 30.0);
}

#[test]
fn test_get_refreshes_lru_order_before_eviction() {
let mut cache = TextMeasurementCache::new(2);
let key_a = TextCacheKey::new("A", "Sans", 12.0, None);
let key_b = TextCacheKey::new("B", "Sans", 12.0, None);
let key_c = TextCacheKey::new("C", "Sans", 12.0, None);

cache.insert(key_a.clone(), measurement(10.0));
cache.insert(key_b.clone(), measurement(20.0));
assert_eq!(cache.get(&key_a).unwrap().ink_width, 10.0);
cache.insert(key_c.clone(), measurement(30.0));

assert!(cache.get(&key_b).is_none());
assert_eq!(cache.get(&key_a).unwrap().ink_width, 10.0);
assert_eq!(cache.get(&key_c).unwrap().ink_width, 30.0);
}

#[test]
fn test_insert_existing_key_updates_cached_measurement() {
let mut cache = TextMeasurementCache::new(2);
let key = TextCacheKey::new("A", "Sans", 12.0, None);

cache.insert(key.clone(), measurement(10.0));
cache.insert(key.clone(), measurement(42.0));

assert_eq!(cache.get(&key).unwrap().ink_width, 42.0);
assert_eq!(cache.entries.len(), 1);
}

#[test]
fn test_empty_text_returns_none() {
let result = measure_text_cached("", "Sans 12", 12.0, None);
Expand Down
Loading
Loading