Skip to content

Commit f63b77c

Browse files
committed
test: expand Rust coverage for Silver
1 parent a8e3d8e commit f63b77c

8 files changed

Lines changed: 1223 additions & 10 deletions

File tree

src/app.rs

Lines changed: 685 additions & 5 deletions
Large diffs are not rendered by default.

src/config.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,4 +567,142 @@ mod tests {
567567
assert_eq!(slot.command, command);
568568
fs::remove_file(config_path).expect("remove config");
569569
}
570+
571+
#[test]
572+
fn default_path_points_inside_localappdata() {
573+
let path = Config::default_path().expect("LOCALAPPDATA is set on Windows");
574+
assert!(path.ends_with(std::path::Path::new("DevNav").join("config.tsv")));
575+
}
576+
577+
#[test]
578+
fn load_returns_defaults_when_the_file_is_missing() {
579+
let unique = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time").as_nanos();
580+
let missing = std::env::temp_dir().join(format!("devnav-missing-{unique}")).join("c.tsv");
581+
let config = Config::load(&missing).expect("missing file yields defaults");
582+
assert!(config.root().is_none());
583+
assert!(config.show_favorites());
584+
assert_eq!(config.language(), None);
585+
assert!(config.configured_shortcuts().is_empty());
586+
}
587+
588+
#[test]
589+
fn load_propagates_real_io_errors_instead_of_swallowing_them() {
590+
let unique = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time").as_nanos();
591+
let directory = std::env::temp_dir().join(format!("devnav-load-dir-{unique}"));
592+
fs::create_dir_all(&directory).expect("create directory");
593+
assert!(Config::load(&directory).is_err());
594+
fs::remove_dir_all(directory).expect("clean directory");
595+
}
596+
597+
#[test]
598+
fn parse_tsv_reads_every_supported_record_type() {
599+
let config = Config::parse_tsv(
600+
"root\tC:\\code\nshow_favorites\tfalse\ncheck_updates\ttrue\nlanguage\ten-US\n\
601+
favorite\tC:\\code\\dev-nav\nalias\tC:\\code\\dev-nav\tprincipal\n\
602+
shortcut\t3\tDev\tbun run dev\nunknown\trecord\n\n",
603+
);
604+
assert_eq!(config.root(), Some(std::path::Path::new("C:\\code")));
605+
assert!(!config.show_favorites());
606+
assert_eq!(config.check_updates(), Some(true));
607+
assert_eq!(config.language(), Some("en-US"));
608+
assert!(config.is_favorite(std::path::Path::new("C:\\code\\dev-nav")));
609+
assert_eq!(config.alias(std::path::Path::new("C:\\code\\dev-nav")), Some("principal"));
610+
assert_eq!(config.shortcut(3).map(|s| s.command.as_str()), Some("bun run dev"));
611+
}
612+
613+
#[test]
614+
fn parse_tsv_rejects_invalid_languages_and_shortcut_records() {
615+
let config = Config::parse_tsv(
616+
"language\tfr-FR\ncheck_updates\tbogus\nshortcut\t10\ta\tcmd\n\
617+
shortcut\tx\ta\tcmd\nshortcut\t2\talias-only-no-tab\nshortcut\t4\t\t \n",
618+
);
619+
assert_eq!(config.language(), None);
620+
assert_eq!(config.check_updates(), Some(false));
621+
assert!(config.shortcut(10).is_none());
622+
assert!(config.shortcut(2).is_none());
623+
assert!(config.shortcut(4).is_none());
624+
}
625+
626+
#[test]
627+
fn toggle_favorite_adds_then_removes_the_path() {
628+
let mut config = Config::default();
629+
let path = std::path::Path::new("C:\\code\\dev-nav");
630+
assert!(config.toggle_favorite(path));
631+
assert!(config.is_favorite(path));
632+
assert!(config.favorite_paths().any(|favorite| favorite == path));
633+
assert!(!config.toggle_favorite(path));
634+
assert!(!config.is_favorite(path));
635+
}
636+
637+
#[test]
638+
fn set_alias_trims_and_removes_on_blank_values() {
639+
let mut config = Config::default();
640+
let path = std::path::PathBuf::from("C:\\code\\dev-nav");
641+
config.set_alias(path.clone(), " principal ".into());
642+
assert_eq!(config.alias(&path), Some("principal"));
643+
config.set_alias(path.clone(), " ".into());
644+
assert_eq!(config.alias(&path), None);
645+
config.set_alias(path.clone(), String::new());
646+
assert_eq!(config.alias(&path), None);
647+
}
648+
649+
#[test]
650+
fn set_language_accepts_only_supported_locales() {
651+
let mut config = Config::default();
652+
config.set_language("en-US");
653+
assert_eq!(config.language(), Some("en-US"));
654+
config.set_language("de-DE");
655+
assert_eq!(config.language(), Some("en-US"));
656+
}
657+
658+
#[test]
659+
fn toggle_update_checks_flips_the_stored_value() {
660+
let mut config = Config::default();
661+
assert!(!config.toggle_update_checks());
662+
assert_eq!(config.check_updates(), Some(false));
663+
assert!(config.toggle_update_checks());
664+
assert_eq!(config.check_updates(), Some(true));
665+
}
666+
667+
#[test]
668+
fn configured_shortcuts_are_ordered_by_slot_index() {
669+
let mut config = Config::default();
670+
config.set_shortcut(7, None, "seven".into());
671+
config.set_shortcut(2, None, "two".into());
672+
config.set_shortcut(5, None, "five".into());
673+
let slots: Vec<u8> =
674+
config.configured_shortcuts().iter().map(|(index, _)| *index).collect();
675+
assert_eq!(slots, vec![2, 5, 7]);
676+
}
677+
678+
#[test]
679+
fn save_creates_missing_parent_directories_and_round_trips() {
680+
let unique = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time").as_nanos();
681+
let sandbox = std::env::temp_dir().join(format!("devnav-nested-{unique}"));
682+
let config_path = sandbox.join("deep").join("deeper").join("config.tsv");
683+
let mut config = Config::default();
684+
config.set_root(std::path::PathBuf::from("C:\\code"));
685+
config.set_alias(std::path::PathBuf::from("C:\\code\\tab\tsheet"), "con\ttab".into());
686+
config.save(&config_path).expect("save into nested directory");
687+
688+
let loaded = Config::load(&config_path).expect("load");
689+
assert_eq!(loaded.root(), Some(std::path::Path::new("C:\\code")));
690+
assert_eq!(loaded.alias(std::path::Path::new("C:\\code\\tab\tsheet")), Some("con\ttab"));
691+
fs::remove_dir_all(sandbox).expect("clean sandbox");
692+
}
693+
694+
#[test]
695+
fn save_replaces_an_existing_config_atomically() {
696+
let unique = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time").as_nanos();
697+
let config_path = std::env::temp_dir().join(format!("devnav-replace-{unique}.tsv"));
698+
let mut config = Config::default();
699+
config.save(&config_path).expect("initial save");
700+
config.set_shortcut(1, Some("Dev".into()), "bun run dev".into());
701+
config.save(&config_path).expect("replace existing file");
702+
assert_eq!(
703+
Config::load(&config_path).expect("load").shortcut(1).map(|s| s.command.as_str()),
704+
Some("bun run dev")
705+
);
706+
fs::remove_file(config_path).expect("remove config");
707+
}
570708
}

src/i18n.rs

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,9 @@ pub fn official_bindings() -> &'static [KeyBinding] {
270270
#[cfg(test)]
271271
mod tests {
272272
use super::{
273-
KeyBinding, KeyToken, Locale, Modifier, format_binding, official_bindings,
274-
resolve_preferred_tags,
273+
KeyBinding, KeyToken, Locale, Modifier, TextId, delete_footer, editor_footer,
274+
editor_footer_compact, editor_title, format_binding, manager_footer,
275+
manager_footer_compact, official_bindings, resolve_preferred_tags, shift_range, text,
275276
};
276277

277278
#[test]
@@ -305,4 +306,115 @@ mod tests {
305306
assert_eq!(resolve_preferred_tags(["de-DE", "es-ES", "en-US"]), Locale::EsEs);
306307
assert_eq!(resolve_preferred_tags(["de-DE", "fr-FR"]), Locale::EnUs);
307308
}
309+
310+
#[test]
311+
fn locale_tags_and_alternation_are_symmetric() {
312+
assert_eq!(Locale::EsEs.tag(), "es-ES");
313+
assert_eq!(Locale::EnUs.tag(), "en-US");
314+
assert_eq!(Locale::EsEs.other(), Locale::EnUs);
315+
assert_eq!(Locale::EnUs.other(), Locale::EsEs);
316+
assert_eq!(Locale::EsEs.other().other(), Locale::EsEs);
317+
}
318+
319+
#[test]
320+
fn every_defined_text_id_is_translated_in_both_locales() {
321+
let cases: [(TextId, &str, &str); 14] = [
322+
(TextId::ManagerTitle, "COMANDOS PERSONALIZADOS", "CUSTOM COMMANDS"),
323+
(TextId::DeleteTitle, "ELIMINAR COMANDO", "REMOVE COMMAND"),
324+
(TextId::Empty, "Vacío", "Empty"),
325+
(TextId::AliasOptional, "Alias (opcional)", "Alias (optional)"),
326+
(TextId::Command, "Comando", "Command"),
327+
(TextId::Save, "Guardar", "Save"),
328+
(TextId::Cancel, "Cancelar", "Cancel"),
329+
(TextId::Delete, "Eliminar", "Delete"),
330+
(TextId::ConfirmDelete, "¿Eliminar comando?", "Remove command?"),
331+
(TextId::SaveError, "No se pudo guardar", "Could not save"),
332+
(TextId::DeleteError, "No se pudo eliminar", "Could not delete"),
333+
(TextId::CommandSaved, "Comando guardado", "Command saved"),
334+
(TextId::CommandDeleted, "Comando eliminado", "Command deleted"),
335+
(TextId::ManageCommands, "Gestionar comandos personalizados", "Manage custom commands"),
336+
];
337+
for (id, expected_es, expected_en) in cases {
338+
assert_eq!(text(Locale::EsEs, id), expected_es, "es-ES {id:?}");
339+
assert_eq!(text(Locale::EnUs, id), expected_en, "en-US {id:?}");
340+
}
341+
// Identifiers without UI copy resolve to an empty string.
342+
assert_eq!(text(Locale::EsEs, TextId::Help), "");
343+
assert_eq!(text(Locale::EnUs, TextId::Quit), "");
344+
}
345+
346+
#[test]
347+
fn shift_range_and_panel_copy_are_localized() {
348+
assert_eq!(shift_range(Locale::EsEs), "Mayús+1–9");
349+
assert_eq!(shift_range(Locale::EnUs), "Shift+1–9");
350+
assert!(manager_footer(Locale::EsEs).contains("Supr"));
351+
assert!(manager_footer(Locale::EnUs).contains("Delete"));
352+
assert!(manager_footer_compact(Locale::EsEs).contains("Supr"));
353+
assert!(manager_footer_compact(Locale::EnUs).contains("Del"));
354+
assert!(editor_footer(Locale::EsEs).contains("Tab"));
355+
assert!(editor_footer(Locale::EnUs).contains("Tab"));
356+
assert_eq!(editor_footer_compact(Locale::EsEs), "Tab · Enter · Esc");
357+
assert_eq!(editor_footer_compact(Locale::EnUs), "Tab · Enter · Esc");
358+
assert!(delete_footer(Locale::EsEs).contains("Confirmar"));
359+
assert!(delete_footer(Locale::EnUs).contains("Confirm"));
360+
}
361+
362+
#[test]
363+
fn editor_title_distinguishes_new_from_edit_in_both_locales() {
364+
assert_eq!(editor_title(Locale::EsEs, true), "NUEVO COMANDO");
365+
assert_eq!(editor_title(Locale::EsEs, false), "EDITAR COMANDO");
366+
assert_eq!(editor_title(Locale::EnUs, true), "NEW COMMAND");
367+
assert_eq!(editor_title(Locale::EnUs, false), "EDIT COMMAND");
368+
}
369+
370+
#[test]
371+
fn format_binding_covers_modifiers_and_special_keys() {
372+
let cases: [(KeyBinding, Locale, &str); 18] = [
373+
(
374+
KeyBinding::with_modifier(Modifier::Ctrl, KeyToken::Char('S')),
375+
Locale::EsEs,
376+
"Ctrl+S",
377+
),
378+
(
379+
KeyBinding::with_modifier(Modifier::Ctrl, KeyToken::Char('U')),
380+
Locale::EnUs,
381+
"Ctrl+U",
382+
),
383+
(
384+
KeyBinding::with_modifier(Modifier::Shift, KeyToken::Char('F')),
385+
Locale::EsEs,
386+
"Mayús+F",
387+
),
388+
(
389+
KeyBinding::with_modifier(Modifier::Shift, KeyToken::Char('F')),
390+
Locale::EnUs,
391+
"Shift+F",
392+
),
393+
(KeyBinding::plain(KeyToken::Char('q')), Locale::EsEs, "q"),
394+
(KeyBinding::plain(KeyToken::F1), Locale::EsEs, "F1"),
395+
(KeyBinding::plain(KeyToken::F2), Locale::EnUs, "F2"),
396+
(KeyBinding::plain(KeyToken::F3), Locale::EsEs, "F3"),
397+
(KeyBinding::plain(KeyToken::Enter), Locale::EnUs, "Enter"),
398+
(KeyBinding::plain(KeyToken::Escape), Locale::EsEs, "Esc"),
399+
(KeyBinding::plain(KeyToken::Up), Locale::EsEs, "↑"),
400+
(KeyBinding::plain(KeyToken::Down), Locale::EnUs, "↓"),
401+
(KeyBinding::plain(KeyToken::Left), Locale::EsEs, "←"),
402+
(KeyBinding::plain(KeyToken::Right), Locale::EnUs, "→"),
403+
(KeyBinding::plain(KeyToken::Backspace), Locale::EsEs, "Retroceso"),
404+
(KeyBinding::plain(KeyToken::Backspace), Locale::EnUs, "Backspace"),
405+
(
406+
KeyBinding::with_modifier(Modifier::Shift, KeyToken::Enter),
407+
Locale::EnUs,
408+
"Shift+Enter",
409+
),
410+
(
411+
KeyBinding::with_modifier(Modifier::Ctrl, KeyToken::Backspace),
412+
Locale::EsEs,
413+
"Ctrl+Retroceso",
414+
),
415+
];
416+
for (binding, locale, expected) in cases {
417+
assert_eq!(format_binding(binding, locale), expected);
418+
}
419+
}
308420
}

src/input.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,14 @@ fn shortcut_slot(virtual_key: u16) -> Option<u8> {
139139

140140
#[cfg(test)]
141141
mod tests {
142-
use super::{Key, VK_DOWN, VK_ESCAPE, VK_F1, VK_F2, VK_F3, VK_UP, map_key};
142+
use super::{
143+
Key, VK_BACK, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F2, VK_F3, VK_HOME, VK_LEFT,
144+
VK_RETURN, VK_RIGHT, VK_TAB, VK_UP, map_key, shortcut_slot,
145+
};
143146
use windows_sys::Win32::System::Console::KEY_EVENT_RECORD;
144-
use windows_sys::Win32::System::Console::{LEFT_CTRL_PRESSED, SHIFT_PRESSED};
147+
use windows_sys::Win32::System::Console::{
148+
LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, SHIFT_PRESSED,
149+
};
145150

146151
fn key_event(virtual_key: u16) -> KEY_EVENT_RECORD {
147152
KEY_EVENT_RECORD {
@@ -152,6 +157,12 @@ mod tests {
152157
}
153158
}
154159

160+
fn key_event_with_char(virtual_key: u16, character: char) -> KEY_EVENT_RECORD {
161+
let mut event = key_event(virtual_key);
162+
event.uChar.UnicodeChar = character as u16;
163+
event
164+
}
165+
155166
#[test]
156167
fn navigation_keys_are_not_escape() {
157168
assert_eq!(map_key(key_event(VK_UP)), Key::Up);
@@ -221,4 +232,57 @@ mod tests {
221232
shifted.dwControlKeyState = SHIFT_PRESSED;
222233
assert_eq!(map_key(shifted), Key::Shortcut(1));
223234
}
235+
236+
#[test]
237+
fn editing_and_function_keys_map_to_dedicated_variants() {
238+
assert_eq!(map_key(key_event(VK_LEFT)), Key::Left);
239+
assert_eq!(map_key(key_event(VK_RIGHT)), Key::Right);
240+
assert_eq!(map_key(key_event(VK_RETURN)), Key::Enter);
241+
assert_eq!(map_key(key_event(VK_BACK)), Key::Backspace);
242+
assert_eq!(map_key(key_event(VK_DELETE)), Key::Delete);
243+
assert_eq!(map_key(key_event(VK_HOME)), Key::Home);
244+
assert_eq!(map_key(key_event(VK_END)), Key::End);
245+
assert_eq!(map_key(key_event(VK_TAB)), Key::Tab);
246+
}
247+
248+
#[test]
249+
fn control_c_maps_to_a_dedicated_quit_signal() {
250+
let mut event = key_event(u16::from(b'C'));
251+
event.dwControlKeyState = LEFT_CTRL_PRESSED;
252+
assert_eq!(map_key(event), Key::CtrlC);
253+
}
254+
255+
#[test]
256+
fn printable_characters_map_through_including_unicode() {
257+
assert_eq!(map_key(key_event_with_char(u16::from(b'A'), 'a')), Key::Char('a'));
258+
assert_eq!(map_key(key_event_with_char(0x31, 'ñ')), Key::Char('ñ'));
259+
assert_eq!(map_key(key_event_with_char(0x4E, '界')), Key::Char('界'));
260+
}
261+
262+
#[test]
263+
fn control_characters_without_a_virtual_key_are_unknown() {
264+
assert_eq!(map_key(key_event_with_char(0x41, '\u{1}')), Key::Unknown);
265+
assert_eq!(map_key(key_event_with_char(0x41, '\u{7f}')), Key::Unknown);
266+
}
267+
268+
#[test]
269+
fn alt_shift_digit_stays_available_for_the_os() {
270+
let mut event = key_event_with_char(u16::from(b'1'), '!');
271+
event.dwControlKeyState = SHIFT_PRESSED | LEFT_ALT_PRESSED;
272+
assert_ne!(map_key(event), Key::Shortcut(1));
273+
274+
let mut event = key_event_with_char(u16::from(b'2'), '@');
275+
event.dwControlKeyState = SHIFT_PRESSED | RIGHT_ALT_PRESSED;
276+
assert_ne!(map_key(event), Key::Shortcut(2));
277+
}
278+
279+
#[test]
280+
fn shortcut_slot_only_accepts_top_row_digits_one_to_nine() {
281+
assert_eq!(shortcut_slot(0x30), None);
282+
assert_eq!(shortcut_slot(0x31), Some(1));
283+
assert_eq!(shortcut_slot(0x39), Some(9));
284+
assert_eq!(shortcut_slot(0x3A), None);
285+
assert_eq!(shortcut_slot(0x2F), None);
286+
assert_eq!(shortcut_slot(0x61), None);
287+
}
224288
}

0 commit comments

Comments
 (0)