diff --git a/AGENTS.md b/AGENTS.md index c4832360..05c4bc40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,31 @@ When adding features, preserve these boundaries before optimizing code layout. ## Coding Conventions +### Test Organization + +- Name the outer inline test module `tests`. +- Group tests under a module named after the function or method under test. +- When a file defines multiple production types, add a type-level module before + the function-level module. +- Name test functions after the behavior or scenario being verified. Do not use + generic names such as `test` or repeat the function name in a `test_*` prefix. + +For example: + +```rust +#[cfg(test)] +mod tests { + mod widget_viewport { + mod scroll_to_include { + #[test] + fn does_not_scroll_while_the_position_is_visible() { + // ... + } + } + } +} +``` + ### Feature Wiring - Control module exposure via feature flags. diff --git a/examples/csv/src/csv.rs b/examples/csv/src/csv.rs index 998fd651..caed8e48 100644 --- a/examples/csv/src/csv.rs +++ b/examples/csv/src/csv.rs @@ -230,67 +230,79 @@ mod tests { ) } - #[test] - fn arrow_keys_move_on_both_axes() { - let mut viewer = viewer(); - viewer.table.create_graphemes_in_viewport(4, 2); + mod csv_viewer { + use super::*; - viewer - .handle_event(&Event::Key(KeyEvent::new( - KeyCode::Down, - KeyModifiers::NONE, - ))) - .unwrap(); - assert_eq!(viewer.table.document.position(), 1); + mod handle_event { + use super::*; - viewer - .handle_event(&Event::Key(KeyEvent::new( - KeyCode::Right, - KeyModifiers::NONE, - ))) - .unwrap(); - assert_eq!( - viewer.table.document.horizontal_offset(), - KEY_HORIZONTAL_SCROLL_CELLS - ); - } + #[test] + fn arrow_keys_move_on_both_axes() { + let mut viewer = viewer(); + viewer.table.create_graphemes_in_viewport(4, 2); - #[test] - fn macos_horizontal_scroll_left_reveals_content_on_the_right() { - let mut viewer = viewer(); - viewer.table.create_graphemes_in_viewport(4, 2); - viewer - .handle_event(&Event::Mouse(MouseEvent { - kind: MouseEventKind::ScrollLeft, - column: 0, - row: 0, - modifiers: KeyModifiers::NONE, - })) - .unwrap(); + viewer + .handle_event(&Event::Key(KeyEvent::new( + KeyCode::Down, + KeyModifiers::NONE, + ))) + .unwrap(); + assert_eq!(viewer.table.document.position(), 1); - assert_eq!( - viewer.table.document.horizontal_offset(), - MOUSE_HORIZONTAL_SCROLL_CELLS - ); + viewer + .handle_event(&Event::Key(KeyEvent::new( + KeyCode::Right, + KeyModifiers::NONE, + ))) + .unwrap(); + assert_eq!( + viewer.table.document.horizontal_offset(), + KEY_HORIZONTAL_SCROLL_CELLS + ); + } - viewer - .handle_event(&Event::Mouse(MouseEvent { - kind: MouseEventKind::ScrollRight, - column: 0, - row: 0, - modifiers: KeyModifiers::NONE, - })) - .unwrap(); - assert_eq!(viewer.table.document.horizontal_offset(), 0); + #[test] + fn macos_horizontal_scroll_left_reveals_content_on_the_right() { + let mut viewer = viewer(); + viewer.table.create_graphemes_in_viewport(4, 2); + viewer + .handle_event(&Event::Mouse(MouseEvent { + kind: MouseEventKind::ScrollLeft, + column: 0, + row: 0, + modifiers: KeyModifiers::NONE, + })) + .unwrap(); + + assert_eq!( + viewer.table.document.horizontal_offset(), + MOUSE_HORIZONTAL_SCROLL_CELLS + ); + + viewer + .handle_event(&Event::Mouse(MouseEvent { + kind: MouseEventKind::ScrollRight, + column: 0, + row: 0, + modifiers: KeyModifiers::NONE, + })) + .unwrap(); + assert_eq!(viewer.table.document.horizontal_offset(), 0); + } + } } - #[test] - fn loads_the_large_csv_fixture_from_a_file() { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../promkit-widgets/benches/table.csv"); - let document = parse_document(&Args { input: Some(path) }).unwrap(); + mod parse_document { + use super::*; + + #[test] + fn loads_the_large_csv_fixture_from_a_file() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../promkit-widgets/benches/table.csv"); + let document = parse_document(&Args { input: Some(path) }).unwrap(); - assert_eq!(document.row_count(), 47_852); - assert_eq!(document.column_count(), 12); + assert_eq!(document.row_count(), 47_852); + assert_eq!(document.column_count(), 12); + } } } diff --git a/examples/repl/src/repl.rs b/examples/repl/src/repl.rs index 67a7ed4e..3e25abe7 100644 --- a/examples/repl/src/repl.rs +++ b/examples/repl/src/repl.rs @@ -402,69 +402,85 @@ mod tests { use super::*; use promkit_widgets::core::Widget; - #[test] - fn completes_only_nonempty_balanced_input() { - assert_eq!(completion(""), Completion::Empty); - assert_eq!(completion("value {"), Completion::Incomplete); - assert_eq!(completion("value {\n [item]\n}"), Completion::Complete); - assert_eq!(completion("{]"), Completion::Invalid); + mod completion { + use super::*; + + #[test] + fn classifies_empty_balanced_unbalanced_and_invalid_input() { + assert_eq!(completion(""), Completion::Empty); + assert_eq!(completion("value {"), Completion::Incomplete); + assert_eq!(completion("value {\n [item]\n}"), Completion::Complete); + assert_eq!(completion("{]"), Completion::Invalid); + } } - #[test] - fn enter_continues_unclosed_input_with_indentation() { - let mut editor = text_editor::TextEditor::new("{"); + mod enter { + use super::*; - assert_eq!(enter(&mut editor), Control::Continue); - assert_eq!(editor.text_without_cursor().to_string(), "{\n "); - } + #[test] + fn continues_unclosed_input_with_indentation() { + let mut editor = text_editor::TextEditor::new("{"); - #[test] - fn enter_submits_complete_single_line_input() { - let mut editor = text_editor::TextEditor::new("value"); + assert_eq!(enter(&mut editor), Control::Continue); + assert_eq!(editor.text_without_cursor().to_string(), "{\n "); + } - assert_eq!(enter(&mut editor), Control::Submit("value".to_string())); - } + #[test] + fn submits_complete_single_line_input() { + let mut editor = text_editor::TextEditor::new("value"); - fn assert_blank_continuation_line_submits(input: &str) { - let mut editor = text_editor::TextEditor::new(input); + assert_eq!(enter(&mut editor), Control::Submit("value".to_string())); + } - assert_eq!(enter(&mut editor), Control::Continue); - assert_eq!( - editor.text_without_cursor().to_string(), - format!("{input}\n") - ); - assert_eq!(enter(&mut editor), Control::Submit(format!("{input}\n"))); - } + fn assert_blank_continuation_line_submits(input: &str) { + let mut editor = text_editor::TextEditor::new(input); - #[test] - fn empty_continuation_line_submits_a_curly_bracket_block() { - assert_blank_continuation_line_submits("{\n value\n}"); - } + assert_eq!(enter(&mut editor), Control::Continue); + assert_eq!( + editor.text_without_cursor().to_string(), + format!("{input}\n") + ); + assert_eq!(enter(&mut editor), Control::Submit(format!("{input}\n"))); + } + + #[test] + fn empty_continuation_line_submits_a_curly_bracket_block() { + assert_blank_continuation_line_submits("{\n value\n}"); + } - #[test] - fn empty_continuation_line_submits_a_square_bracket_block() { - assert_blank_continuation_line_submits("[\n value\n]"); + #[test] + fn empty_continuation_line_submits_a_square_bracket_block() { + assert_blank_continuation_line_submits("[\n value\n]"); + } } - #[test] - fn continuation_prefix_is_only_presentational() { - let state = text_editor::State { - texteditor: text_editor::TextEditor::new("first\nsecond"), - config: text_editor::Config { - prefix: "❯❯❯ ".into(), - continuation_prefix: "... ".into(), - ..Default::default() - }, - ..Default::default() - }; + mod text_editor_state { + use super::*; + + mod create_graphemes { + use super::*; + + #[test] + fn continuation_prefix_is_only_presentational() { + let state = text_editor::State { + texteditor: text_editor::TextEditor::new("first\nsecond"), + config: text_editor::Config { + prefix: "❯❯❯ ".into(), + continuation_prefix: "... ".into(), + ..Default::default() + }, + ..Default::default() + }; - assert_eq!( - state.create_graphemes().graphemes.to_string(), - "❯❯❯ first\n... second " - ); - assert_eq!( - state.texteditor.text_without_cursor().to_string(), - "first\nsecond" - ); + assert_eq!( + state.create_graphemes().graphemes.to_string(), + "❯❯❯ first\n... second " + ); + assert_eq!( + state.texteditor.text_without_cursor().to_string(), + "first\nsecond" + ); + } + } } } diff --git a/promkit-core/src/grapheme.rs b/promkit-core/src/grapheme.rs index 0351097a..53f6f9d4 100644 --- a/promkit-core/src/grapheme.rs +++ b/promkit-core/src/grapheme.rs @@ -448,331 +448,335 @@ impl fmt::Display for StyledGraphemesDisplay<'_> { } #[cfg(test)] -mod test { +mod tests { use super::*; - mod from_str { + mod styled_graphemes { use super::*; - #[test] - fn test() { - let style = ContentStyle::default(); - let graphemes = StyledGraphemes::from_str("abc", style.clone()); - assert_eq!(3, graphemes.0.len()); - assert!(graphemes.0.iter().all(|g| g.style == style)); + mod from_str { + use super::*; + + #[test] + fn creates_graphemes_with_the_given_style() { + let style = ContentStyle::default(); + let graphemes = StyledGraphemes::from_str("abc", style); + assert_eq!(3, graphemes.0.len()); + assert!(graphemes.0.iter().all(|g| g.style == style)); + } } - } - mod from_lines { - use super::*; + mod from_lines { + use super::*; - #[test] - fn test_empty() { - let g = StyledGraphemes::from_lines(Vec::new()); - assert!(g.is_empty()); - } + #[test] + fn empty_input_returns_empty_graphemes() { + let g = StyledGraphemes::from_lines(Vec::new()); + assert!(g.is_empty()); + } - #[test] - fn test_join() { - let g = StyledGraphemes::from_lines(vec![ - StyledGraphemes::from("abc"), - StyledGraphemes::from("def"), - ]); - assert_eq!("abc\ndef", g.to_string()); + #[test] + fn inserts_newlines_between_lines() { + let g = StyledGraphemes::from_lines(vec![ + StyledGraphemes::from("abc"), + StyledGraphemes::from("def"), + ]); + assert_eq!("abc\ndef", g.to_string()); + } } - } - mod chars { - use super::*; + mod chars { + use super::*; - #[test] - fn test() { - let graphemes = StyledGraphemes::from("abc"); - let chars = graphemes.chars(); - assert_eq!(vec!['a', 'b', 'c'], chars); + #[test] + fn returns_the_characters() { + let graphemes = StyledGraphemes::from("abc"); + let chars = graphemes.chars(); + assert_eq!(vec!['a', 'b', 'c'], chars); + } } - } - mod widths { - use super::*; + mod widths { + use super::*; - #[test] - fn test() { - let graphemes = StyledGraphemes::from("a b"); - assert_eq!(3, graphemes.widths()); // 'a' and 'b' are each 1 width, and space is 1 width + #[test] + fn sums_display_widths() { + let graphemes = StyledGraphemes::from("a b"); + assert_eq!(3, graphemes.widths()); // 'a' and 'b' are each 1 width, and space is 1 width + } } - } - mod styled_display { - use super::*; + mod styled_display { + use super::*; - #[test] - fn test() { - let graphemes = StyledGraphemes::from("abc"); - let display = graphemes.styled_display(); - assert_eq!(format!("{}", display), "abc"); // Assuming default styles do not alter appearance + #[test] + fn renders_characters_with_default_styles() { + let graphemes = StyledGraphemes::from("abc"); + let display = graphemes.styled_display(); + assert_eq!(format!("{}", display), "abc"); // Assuming default styles do not alter appearance + } } - } - mod apply_style { - use super::*; + mod apply_style { + use super::*; - use crossterm::style::Color; + use crossterm::style::Color; - #[test] - fn test() { - let mut graphemes = StyledGraphemes::from("abc"); - let new_style = ContentStyle { - foreground_color: Some(Color::Green), - ..Default::default() - }; - graphemes = graphemes.apply_style(new_style.clone()); - assert!(graphemes.iter().all(|g| g.style == new_style)); + #[test] + fn applies_the_style_to_every_grapheme() { + let mut graphemes = StyledGraphemes::from("abc"); + let new_style = ContentStyle { + foreground_color: Some(Color::Green), + ..Default::default() + }; + graphemes = graphemes.apply_style(new_style); + assert!(graphemes.iter().all(|g| g.style == new_style)); + } } - } - mod apply_style_at { - use super::*; + mod apply_style_at { + use super::*; - use crossterm::style::Color; + use crossterm::style::Color; - #[test] - fn test_apply_style_at_specific_index() { - let mut graphemes = StyledGraphemes::from("abc"); - let new_style = ContentStyle { - foreground_color: Some(Color::Green), - ..Default::default() - }; - graphemes = graphemes.apply_style_at(1, new_style.clone()); - assert_eq!(graphemes.0[1].style, new_style); - assert_ne!(graphemes.0[0].style, new_style); - assert_ne!(graphemes.0[2].style, new_style); - } - - #[test] - fn test_apply_style_at_out_of_bounds_index() { - let mut graphemes = StyledGraphemes::from("abc"); - let new_style = ContentStyle { - foreground_color: Some(Color::Green), - ..Default::default() - }; - graphemes = graphemes.apply_style_at(5, new_style.clone()); // Out of bounds - assert_eq!(graphemes.0.len(), 3); // Ensure no changes in length + #[test] + fn applies_the_style_at_the_given_index() { + let mut graphemes = StyledGraphemes::from("abc"); + let new_style = ContentStyle { + foreground_color: Some(Color::Green), + ..Default::default() + }; + graphemes = graphemes.apply_style_at(1, new_style); + assert_eq!(graphemes.0[1].style, new_style); + assert_ne!(graphemes.0[0].style, new_style); + assert_ne!(graphemes.0[2].style, new_style); + } + + #[test] + fn ignores_an_out_of_bounds_index() { + let mut graphemes = StyledGraphemes::from("abc"); + let new_style = ContentStyle { + foreground_color: Some(Color::Green), + ..Default::default() + }; + graphemes = graphemes.apply_style_at(5, new_style); // Out of bounds + assert_eq!(graphemes.0.len(), 3); // Ensure no changes in length + } } - } - mod apply_attribute { - use super::*; + mod apply_attribute { + use super::*; - #[test] - fn test() { - let mut graphemes = StyledGraphemes::from("abc"); - graphemes = graphemes.apply_attribute(Attribute::Bold); - assert!( - graphemes - .iter() - .all(|g| g.style.attributes.has(Attribute::Bold)) - ); + #[test] + fn applies_the_attribute_to_every_grapheme() { + let mut graphemes = StyledGraphemes::from("abc"); + graphemes = graphemes.apply_attribute(Attribute::Bold); + assert!( + graphemes + .iter() + .all(|g| g.style.attributes.has(Attribute::Bold)) + ); + } } - } - mod find_all { - use super::*; + mod find_all { + use super::*; - #[test] - fn test_with_empty_query() { - let graphemes = StyledGraphemes::from("Hello, world!"); - let indices = graphemes.find_all(""); - assert!( - indices.is_empty(), - "Should return an empty vector for an empty query string" - ); - } - - #[test] - fn test_with_repeated_substring() { - let graphemes = StyledGraphemes::from("Hello, world! Hello, universe!"); - let indices = graphemes.find_all("Hello"); - assert_eq!( - indices, - vec![0, 14], - "Should find all starting indices of 'Hello'" - ); - } - - #[test] - fn test_with_nonexistent_substring() { - let graphemes = StyledGraphemes::from("Hello, world!"); - let indices = graphemes.find_all("xyz"); - assert!( - indices.is_empty(), - "Should return an empty vector for a non-existent substring" - ); - } - - #[test] - fn test_with_special_character() { - let graphemes = StyledGraphemes::from("µs µs µs"); - let indices = graphemes.find_all("s"); - assert_eq!( - indices, - vec![1, 4, 7], - "Should correctly find indices of substring 'µs'" - ); - } - - #[test] - fn test_with_single_character() { - let graphemes = StyledGraphemes::from("abcabcabc"); - let indices = graphemes.find_all("b"); - assert_eq!( - indices, - vec![1, 4, 7], - "Should find all indices of character 'b'" - ); - } - - #[test] - fn test_with_full_match() { - let graphemes = StyledGraphemes::from("Hello"); - let indices = graphemes.find_all("Hello"); - assert_eq!(indices, vec![0], "Should match the entire string"); - } - - #[test] - fn test_with_partial_overlap() { - let graphemes = StyledGraphemes::from("ababa"); - let indices = graphemes.find_all("aba"); - assert_eq!( - indices, - vec![0, 2], - "Should handle overlapping matches correctly" - ); - } - } - - mod highlight { - use super::*; + #[test] + fn empty_query_returns_no_matches() { + let graphemes = StyledGraphemes::from("Hello, world!"); + let indices = graphemes.find_all(""); + assert!( + indices.is_empty(), + "Should return an empty vector for an empty query string" + ); + } - #[test] - fn test_with_empty_query() { - let graphemes = StyledGraphemes::from("Hello, world!"); - let expected = graphemes.clone(); - let highlighted = graphemes.highlight("", ContentStyle::default()); - assert_eq!(highlighted.unwrap(), expected); - } - } + #[test] + fn finds_repeated_substrings() { + let graphemes = StyledGraphemes::from("Hello, world! Hello, universe!"); + let indices = graphemes.find_all("Hello"); + assert_eq!( + indices, + vec![0, 14], + "Should find all starting indices of 'Hello'" + ); + } - mod replace { - use super::*; + #[test] + fn missing_substring_returns_no_matches() { + let graphemes = StyledGraphemes::from("Hello, world!"); + let indices = graphemes.find_all("xyz"); + assert!( + indices.is_empty(), + "Should return an empty vector for a non-existent substring" + ); + } - #[test] - fn test() { - let graphemes = StyledGraphemes::from("banana"); - assert_eq!("bonono", graphemes.replace("a", "o").to_string()); - } + #[test] + fn handles_multibyte_characters() { + let graphemes = StyledGraphemes::from("µs µs µs"); + let indices = graphemes.find_all("s"); + assert_eq!( + indices, + vec![1, 4, 7], + "Should correctly find indices of substring 'µs'" + ); + } - #[test] - fn test_with_nonexistent_character() { - let graphemes = StyledGraphemes::from("Hello World"); - assert_eq!("Hello World", graphemes.replace("x", "o").to_string()); - } + #[test] + fn finds_single_characters() { + let graphemes = StyledGraphemes::from("abcabcabc"); + let indices = graphemes.find_all("b"); + assert_eq!( + indices, + vec![1, 4, 7], + "Should find all indices of character 'b'" + ); + } - #[test] - fn test_with_empty_string() { - let graphemes = StyledGraphemes::from("Hello World"); - assert_eq!("Hell Wrld", graphemes.replace("o", "").to_string()); - } + #[test] + fn matches_the_entire_input() { + let graphemes = StyledGraphemes::from("Hello"); + let indices = graphemes.find_all("Hello"); + assert_eq!(indices, vec![0], "Should match the entire string"); + } - #[test] - fn test_with_multiple_characters() { - let graphemes = StyledGraphemes::from("Hello World"); - assert_eq!("Hellabc Wabcrld", graphemes.replace("o", "abc").to_string()); + #[test] + fn finds_overlapping_matches() { + let graphemes = StyledGraphemes::from("ababa"); + let indices = graphemes.find_all("aba"); + assert_eq!( + indices, + vec![0, 2], + "Should handle overlapping matches correctly" + ); + } } - } - mod replace_range { - use super::*; + mod highlight { + use super::*; - #[test] - fn test() { - let mut graphemes = StyledGraphemes::from("Hello"); - graphemes.replace_range(1..5, "i"); - assert_eq!("Hi", graphemes.to_string()); + #[test] + fn empty_query_returns_the_input_unchanged() { + let graphemes = StyledGraphemes::from("Hello, world!"); + let expected = graphemes.clone(); + let highlighted = graphemes.highlight("", ContentStyle::default()); + assert_eq!(highlighted.unwrap(), expected); + } } - } - mod wrapped_lines { - use super::*; + mod replace { + use super::*; - #[test] - fn test_empty() { - let input = StyledGraphemes::default(); - let rows = input.wrapped_lines(10); - assert_eq!(rows.len(), 0); - } + #[test] + fn replaces_all_occurrences() { + let graphemes = StyledGraphemes::from("banana"); + assert_eq!("bonono", graphemes.replace("a", "o").to_string()); + } - #[test] - fn test_wrap_by_width() { - let input = StyledGraphemes::from("123456"); - let rows = input.wrapped_lines(3); - assert_eq!(rows.len(), 2); - assert_eq!("123", rows[0].to_string()); - assert_eq!("456", rows[1].to_string()); - } + #[test] + fn missing_pattern_leaves_the_input_unchanged() { + let graphemes = StyledGraphemes::from("Hello World"); + assert_eq!("Hello World", graphemes.replace("x", "o").to_string()); + } - #[test] - fn test_split_by_newline() { - let input = StyledGraphemes::from("ab\ncd"); - let rows = input.wrapped_lines(10); - assert_eq!(rows.len(), 2); - assert_eq!("ab", rows[0].to_string()); - assert_eq!("cd", rows[1].to_string()); - } + #[test] + fn empty_replacement_removes_matches() { + let graphemes = StyledGraphemes::from("Hello World"); + assert_eq!("Hell Wrld", graphemes.replace("o", "").to_string()); + } - #[test] - fn test_trailing_newline() { - let input = StyledGraphemes::from("ab\n"); - let rows = input.wrapped_lines(10); - assert_eq!(rows.len(), 2); - assert_eq!("ab", rows[0].to_string()); - assert_eq!("", rows[1].to_string()); + #[test] + fn longer_replacement_expands_matches() { + let graphemes = StyledGraphemes::from("Hello World"); + assert_eq!("Hellabc Wabcrld", graphemes.replace("o", "abc").to_string()); + } } - } - mod truncated_line_with_ellipsis { - use super::*; + mod replace_range { + use super::*; - #[test] - fn test_no_truncate() { - let input = StyledGraphemes::from("abc"); - let ellipsis = StyledGraphemes::from("…"); - let output = input.truncated_line_with_ellipsis(10, &ellipsis); - assert_eq!("abc", output.to_string()); + #[test] + fn replaces_the_given_range() { + let mut graphemes = StyledGraphemes::from("Hello"); + graphemes.replace_range(1..5, "i"); + assert_eq!("Hi", graphemes.to_string()); + } } - #[test] - fn test_width_zero() { - let input = StyledGraphemes::from("abc"); - let ellipsis = StyledGraphemes::from("…"); - let output = input.truncated_line_with_ellipsis(0, &ellipsis); - assert_eq!("", output.to_string()); - } + mod wrapped_lines { + use super::*; + + #[test] + fn empty_input_returns_no_lines() { + let input = StyledGraphemes::default(); + let rows = input.wrapped_lines(10); + assert_eq!(rows.len(), 0); + } + + #[test] + fn wraps_at_the_display_width() { + let input = StyledGraphemes::from("123456"); + let rows = input.wrapped_lines(3); + assert_eq!(rows.len(), 2); + assert_eq!("123", rows[0].to_string()); + assert_eq!("456", rows[1].to_string()); + } + + #[test] + fn splits_at_explicit_newlines() { + let input = StyledGraphemes::from("ab\ncd"); + let rows = input.wrapped_lines(10); + assert_eq!(rows.len(), 2); + assert_eq!("ab", rows[0].to_string()); + assert_eq!("cd", rows[1].to_string()); + } - #[test] - fn test_ellipsis_only() { - let input = StyledGraphemes::from("abc"); - let ellipsis = StyledGraphemes::from("…"); - let output = input.truncated_line_with_ellipsis(1, &ellipsis); - assert_eq!("…", output.to_string()); + #[test] + fn preserves_a_trailing_empty_line() { + let input = StyledGraphemes::from("ab\n"); + let rows = input.wrapped_lines(10); + assert_eq!(rows.len(), 2); + assert_eq!("ab", rows[0].to_string()); + assert_eq!("", rows[1].to_string()); + } } - #[test] - fn test_truncate() { - let input = StyledGraphemes::from("abcdef"); - let ellipsis = StyledGraphemes::from("…"); - let output = input.truncated_line_with_ellipsis(4, &ellipsis); - assert_eq!("abc…", output.to_string()); + mod truncated_line_with_ellipsis { + use super::*; + + #[test] + fn returns_the_input_when_it_fits() { + let input = StyledGraphemes::from("abc"); + let ellipsis = StyledGraphemes::from("…"); + let output = input.truncated_line_with_ellipsis(10, &ellipsis); + assert_eq!("abc", output.to_string()); + } + + #[test] + fn zero_width_returns_empty_graphemes() { + let input = StyledGraphemes::from("abc"); + let ellipsis = StyledGraphemes::from("…"); + let output = input.truncated_line_with_ellipsis(0, &ellipsis); + assert_eq!("", output.to_string()); + } + + #[test] + fn returns_only_the_ellipsis_when_no_content_fits() { + let input = StyledGraphemes::from("abc"); + let ellipsis = StyledGraphemes::from("…"); + let output = input.truncated_line_with_ellipsis(1, &ellipsis); + assert_eq!("…", output.to_string()); + } + + #[test] + fn truncates_content_and_appends_the_ellipsis() { + let input = StyledGraphemes::from("abcdef"); + let ellipsis = StyledGraphemes::from("…"); + let output = input.truncated_line_with_ellipsis(4, &ellipsis); + assert_eq!("abc…", output.to_string()); + } } } } diff --git a/promkit-core/src/render.rs b/promkit-core/src/render.rs index e00b5097..0052484b 100644 --- a/promkit-core/src/render.rs +++ b/promkit-core/src/render.rs @@ -254,54 +254,58 @@ mod tests { use super::*; use crate::{grapheme::StyledGraphemes, widget::WidgetViewport}; - #[test] - fn hit_test_and_screen_position_round_trip() { - let renderer = Renderer { - terminal: AsyncMutex::new(Terminal::new((0, 0))), - contents: SkipMap::new(), - layout_engine: Mutex::new(RendererLayout::default()), - layout: RwLock::new(Some(LayoutSnapshot { - origin: ScreenPosition { row: 3, column: 0 }, - terminal_width: 20, - entries: vec![layout::LayoutEntry { - index: 7usize, - viewport: WidgetViewport { - screen_row: 3, - height: 2, - content_row: 1, - }, - rows: vec![ - layout::VisualRow { - content_row: 0, - content_column: 0, - graphemes: StyledGraphemes::from("hidden"), - }, - layout::VisualRow { + mod hit_test { + use super::*; + + #[test] + fn maps_screen_positions_back_to_widget_positions() { + let renderer = Renderer { + terminal: AsyncMutex::new(Terminal::new((0, 0))), + contents: SkipMap::new(), + layout_engine: Mutex::new(RendererLayout::default()), + layout: RwLock::new(Some(LayoutSnapshot { + origin: ScreenPosition { row: 3, column: 0 }, + terminal_width: 20, + entries: vec![layout::LayoutEntry { + index: 7usize, + viewport: WidgetViewport { + screen_row: 3, + height: 2, content_row: 1, - content_column: 0, - graphemes: StyledGraphemes::from("first"), }, - layout::VisualRow { - content_row: 2, - content_column: 0, - graphemes: StyledGraphemes::from("second"), - }, - ], - }], - })), - last_terminal_size: Mutex::new(None), - }; + rows: vec![ + layout::VisualRow { + content_row: 0, + content_column: 0, + graphemes: StyledGraphemes::from("hidden"), + }, + layout::VisualRow { + content_row: 1, + content_column: 0, + graphemes: StyledGraphemes::from("first"), + }, + layout::VisualRow { + content_row: 2, + content_column: 0, + graphemes: StyledGraphemes::from("second"), + }, + ], + }], + })), + last_terminal_size: Mutex::new(None), + }; - let screen = ScreenPosition { row: 4, column: 2 }; - let widget = renderer.hit_test(screen).unwrap(); - assert_eq!( - widget, - WidgetPosition { - index: 7, - row: 2, - column: 2, - } - ); - assert_eq!(renderer.screen_position(widget), Some(screen)); + let screen = ScreenPosition { row: 4, column: 2 }; + let widget = renderer.hit_test(screen).unwrap(); + assert_eq!( + widget, + WidgetPosition { + index: 7, + row: 2, + column: 2, + } + ); + assert_eq!(renderer.screen_position(widget), Some(screen)); + } } } diff --git a/promkit-core/src/render/layout.rs b/promkit-core/src/render/layout.rs index 70a9e93e..4146f80d 100644 --- a/promkit-core/src/render/layout.rs +++ b/promkit-core/src/render/layout.rs @@ -376,115 +376,139 @@ mod tests { use super::*; use crate::widget::WidgetLayout; - #[test] - fn layout_maps_logical_cursor_to_wrapped_row() { - let created = CreatedGraphemes { - graphemes: StyledGraphemes::from("abcdefghij"), - cursor: Some(ContentPosition { row: 0, column: 8 }), - ..Default::default() - }; - let cursor = created.cursor.unwrap(); - let rows = layout_content(created.graphemes, created.layout.width_mode, 4); - - assert_eq!(rows.len(), 3); - assert_eq!( - visual_position(&rows, cursor), - Some(VisualPosition { row: 2, column: 0 }) - ); + mod visual_position { + use super::*; + + #[test] + fn maps_a_logical_cursor_to_its_wrapped_row() { + let created = CreatedGraphemes { + graphemes: StyledGraphemes::from("abcdefghij"), + cursor: Some(ContentPosition { row: 0, column: 8 }), + ..Default::default() + }; + let cursor = created.cursor.unwrap(); + let rows = layout_content(created.graphemes, created.layout.width_mode, 4); + + assert_eq!(rows.len(), 3); + assert_eq!( + visual_position(&rows, cursor), + Some(VisualPosition { row: 2, column: 0 }) + ); + } } - #[test] - fn wrap_preserves_a_grapheme_wider_than_the_terminal() { - let created = CreatedGraphemes { - graphemes: StyledGraphemes::from("界"), - cursor: Some(ContentPosition { row: 0, column: 0 }), - ..Default::default() - }; - - let cursor = created.cursor.unwrap(); - let rows = layout_content(created.graphemes, created.layout.width_mode, 1); - - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].content_row, 0); - assert_eq!(rows[0].content_column, 0); - assert_eq!(rows[0].graphemes.to_string(), "…"); - assert_eq!( - visual_position(&rows, cursor), - Some(VisualPosition { row: 0, column: 0 }) - ); - } + mod wrap_line { + use super::*; - #[test] - fn wrap_preserves_columns_after_a_grapheme_wider_than_the_terminal() { - let created = CreatedGraphemes { - graphemes: StyledGraphemes::from("界a"), - cursor: Some(ContentPosition { row: 0, column: 2 }), - ..Default::default() - }; - - let cursor = created.cursor.unwrap(); - let rows = layout_content(created.graphemes, created.layout.width_mode, 1); - - assert_eq!(rows.len(), 2); - assert_eq!(rows[0].content_column, 0); - assert_eq!(rows[0].graphemes.to_string(), "…"); - assert_eq!(rows[1].content_column, 2); - assert_eq!(rows[1].graphemes.to_string(), "a"); - assert_eq!( - visual_position(&rows, cursor), - Some(VisualPosition { row: 1, column: 0 }) - ); - } + #[test] + fn preserves_a_grapheme_wider_than_the_terminal() { + let created = CreatedGraphemes { + graphemes: StyledGraphemes::from("界"), + cursor: Some(ContentPosition { row: 0, column: 0 }), + ..Default::default() + }; + + let cursor = created.cursor.unwrap(); + let rows = layout_content(created.graphemes, created.layout.width_mode, 1); + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].content_row, 0); + assert_eq!(rows[0].content_column, 0); + assert_eq!(rows[0].graphemes.to_string(), "…"); + assert_eq!( + visual_position(&rows, cursor), + Some(VisualPosition { row: 0, column: 0 }) + ); + } - #[test] - fn truncate_keeps_one_visual_row_per_logical_row() { - let created = CreatedGraphemes { - graphemes: StyledGraphemes::from("abcdefghij\nsecond"), - layout: WidgetLayout { - width_mode: WidthMode::Truncate, + #[test] + fn preserves_columns_after_a_grapheme_wider_than_the_terminal() { + let created = CreatedGraphemes { + graphemes: StyledGraphemes::from("界a"), + cursor: Some(ContentPosition { row: 0, column: 2 }), ..Default::default() - }, - cursor: None, - }; - let rows = layout_content(created.graphemes, created.layout.width_mode, 4); - - assert_eq!(rows.len(), 2); - assert_eq!(rows[0].graphemes.to_string(), "abc…"); - assert_eq!(rows[1].graphemes.to_string(), "sec…"); + }; + + let cursor = created.cursor.unwrap(); + let rows = layout_content(created.graphemes, created.layout.width_mode, 1); + + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].content_column, 0); + assert_eq!(rows[0].graphemes.to_string(), "…"); + assert_eq!(rows[1].content_column, 2); + assert_eq!(rows[1].graphemes.to_string(), "a"); + assert_eq!( + visual_position(&rows, cursor), + Some(VisualPosition { row: 1, column: 0 }) + ); + } } - #[test] - fn layout_preserves_empty_logical_rows() { - let rows = layout_content(StyledGraphemes::from("first\n\n"), WidthMode::Wrap, 80); - let text = rows - .iter() - .map(|row| row.graphemes.to_string()) - .collect::>(); + mod truncate_line { + use super::*; + + #[test] + fn keeps_one_visual_row_per_logical_row() { + let created = CreatedGraphemes { + graphemes: StyledGraphemes::from("abcdefghij\nsecond"), + layout: WidgetLayout { + width_mode: WidthMode::Truncate, + ..Default::default() + }, + cursor: None, + }; + let rows = layout_content(created.graphemes, created.layout.width_mode, 4); - assert_eq!(text, ["first", "", ""]); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].graphemes.to_string(), "abc…"); + assert_eq!(rows[1].graphemes.to_string(), "sec…"); + } } - #[test] - fn allocates_height_and_preserves_viewport_between_frames() { - let created = CreatedGraphemes { - graphemes: StyledGraphemes::from("first\nsecond\nthird"), - layout: WidgetLayout { - max_height: Some(2), - ..Default::default() - }, - cursor: Some(ContentPosition { row: 2, column: 0 }), - }; - let mut layout = RendererLayout::default(); - - let first = layout.layout([(0, created.clone())], 80, 24).unwrap(); - assert_eq!(first.pane_count(), 1); - assert_eq!(first.visual_row_count(), 3); - assert_eq!(first.visible_row_count(), 2); - let first_panes = first.panes(); - assert_eq!(first_panes[0][0].to_string(), "second"); - - let second = layout.layout([(0, created)], 80, 24).unwrap(); - let second_panes = second.panes(); - assert_eq!(second_panes[0][0].to_string(), "second"); + mod into_logical_lines { + use super::*; + + #[test] + fn preserves_empty_logical_rows() { + let rows = layout_content(StyledGraphemes::from("first\n\n"), WidthMode::Wrap, 80); + let text = rows + .iter() + .map(|row| row.graphemes.to_string()) + .collect::>(); + + assert_eq!(text, ["first", "", ""]); + } + } + + mod renderer_layout { + use super::*; + + mod layout { + use super::*; + + #[test] + fn allocates_height_and_preserves_the_viewport_between_frames() { + let created = CreatedGraphemes { + graphemes: StyledGraphemes::from("first\nsecond\nthird"), + layout: WidgetLayout { + max_height: Some(2), + ..Default::default() + }, + cursor: Some(ContentPosition { row: 2, column: 0 }), + }; + let mut layout = RendererLayout::default(); + + let first = layout.layout([(0, created.clone())], 80, 24).unwrap(); + assert_eq!(first.pane_count(), 1); + assert_eq!(first.visual_row_count(), 3); + assert_eq!(first.visible_row_count(), 2); + let first_panes = first.panes(); + assert_eq!(first_panes[0][0].to_string(), "second"); + + let second = layout.layout([(0, created)], 80, 24).unwrap(); + let second_panes = second.panes(); + assert_eq!(second_panes[0][0].to_string(), "second"); + } + } } } diff --git a/promkit-core/src/terminal.rs b/promkit-core/src/terminal.rs index f430fed8..64c6ad85 100644 --- a/promkit-core/src/terminal.rs +++ b/promkit-core/src/terminal.rs @@ -171,100 +171,109 @@ impl Terminal { mod tests { use super::*; - fn rows(count: usize) -> Vec> { - vec![ - (0..count) - .map(|index| StyledGraphemes::from(format!("row {index}"))) - .collect(), - ] - } - - fn command_bytes(command: impl crate::crossterm::Command) -> Vec { - let mut output = Vec::new(); - crossterm::queue!(output, command).unwrap(); - output - } - - fn command_offset(output: &[u8], command: impl crate::crossterm::Command) -> usize { - let command = command_bytes(command); - output - .windows(command.len()) - .position(|window| window == command) - .expect("expected terminal command was not emitted") - } + mod terminal { + use super::*; + + mod draw_rows_to { + use super::*; + use crate::crossterm::terminal as crossterm_terminal; + + fn rows(count: usize) -> Vec> { + vec![ + (0..count) + .map(|index| StyledGraphemes::from(format!("row {index}"))) + .collect(), + ] + } - #[test] - fn growing_frame_scrolls_only_after_clearing_the_previous_frame() { - let mut terminal = Terminal::new((0, 7)); - let mut output = Vec::new(); + fn command_bytes(command: impl crate::crossterm::Command) -> Vec { + let mut output = Vec::new(); + crossterm::queue!(output, command).unwrap(); + output + } - terminal.draw_rows_to(&mut output, &rows(8), 10).unwrap(); + fn command_offset(output: &[u8], command: impl crate::crossterm::Command) -> usize { + let command = command_bytes(command); + output + .windows(command.len()) + .position(|window| window == command) + .expect("expected terminal command was not emitted") + } - let clear = command_offset( - &output, - terminal::Clear(terminal::ClearType::FromCursorDown), - ); - let scroll = command_offset(&output, terminal::ScrollUp(1)); + #[test] + fn growing_frame_scrolls_only_after_clearing_the_previous_frame() { + let mut terminal = Terminal::new((0, 7)); + let mut output = Vec::new(); - assert!(clear < scroll); - assert_eq!(terminal.position, (0, 2)); - } + terminal.draw_rows_to(&mut output, &rows(8), 10).unwrap(); - #[test] - fn shrinking_frame_scrolls_preceding_output_back_down_before_redrawing() { - let mut terminal = Terminal::new((0, 7)); - terminal - .draw_rows_to(&mut Vec::new(), &rows(8), 10) - .unwrap(); - assert_eq!(terminal.position, (0, 2)); + let clear = command_offset( + &output, + crossterm_terminal::Clear(crossterm_terminal::ClearType::FromCursorDown), + ); + let scroll = command_offset(&output, crossterm_terminal::ScrollUp(1)); - let mut output = Vec::new(); - terminal.draw_rows_to(&mut output, &rows(3), 10).unwrap(); + assert!(clear < scroll); + assert_eq!(terminal.position, (0, 2)); + } - let scroll_down = command_offset(&output, terminal::ScrollDown(5)); - let clear = command_offset( - &output, - terminal::Clear(terminal::ClearType::FromCursorDown), - ); - let draw = command_bytes(cursor::MoveTo(0, 7)); - let draw = output - .windows(draw.len()) - .enumerate() - .filter(|(_, window)| *window == draw) - .map(|(offset, _)| offset) - .nth(1) - .expect("expected a move from the clear position to the draw position"); - - assert!(scroll_down < clear); - assert!(clear < draw); - assert_eq!(terminal.position, (0, 7)); - } + #[test] + fn shrinking_frame_scrolls_preceding_output_back_down_before_redrawing() { + let mut terminal = Terminal::new((0, 7)); + terminal + .draw_rows_to(&mut Vec::new(), &rows(8), 10) + .unwrap(); + assert_eq!(terminal.position, (0, 2)); + + let mut output = Vec::new(); + terminal.draw_rows_to(&mut output, &rows(3), 10).unwrap(); + + let scroll_down = command_offset(&output, crossterm_terminal::ScrollDown(5)); + let clear = command_offset( + &output, + crossterm_terminal::Clear(crossterm_terminal::ClearType::FromCursorDown), + ); + let draw = command_bytes(cursor::MoveTo(0, 7)); + let draw = output + .windows(draw.len()) + .enumerate() + .filter(|(_, window)| *window == draw) + .map(|(offset, _)| offset) + .nth(1) + .expect("expected a move from the clear position to the draw position"); + + assert!(scroll_down < clear); + assert!(clear < draw); + assert_eq!(terminal.position, (0, 7)); + } - #[test] - fn draw_frame_controls_wrapping_inside_a_synchronized_update() { - let mut terminal = Terminal::new((0, 0)); - let mut output = Vec::new(); + #[test] + fn draw_frame_controls_wrapping_inside_a_synchronized_update() { + let mut terminal = Terminal::new((0, 0)); + let mut output = Vec::new(); - terminal.draw_rows_to(&mut output, &rows(1), 10).unwrap(); + terminal.draw_rows_to(&mut output, &rows(1), 10).unwrap(); - let begin = command_offset(&output, terminal::BeginSynchronizedUpdate); - let disable_wrap = command_offset(&output, terminal::DisableLineWrap); - let enable_wrap = command_offset(&output, terminal::EnableLineWrap); - let end = command_offset(&output, terminal::EndSynchronizedUpdate); + let begin = command_offset(&output, crossterm_terminal::BeginSynchronizedUpdate); + let disable_wrap = command_offset(&output, crossterm_terminal::DisableLineWrap); + let enable_wrap = command_offset(&output, crossterm_terminal::EnableLineWrap); + let end = command_offset(&output, crossterm_terminal::EndSynchronizedUpdate); - assert!(begin < disable_wrap); - assert!(disable_wrap < enable_wrap); - assert!(enable_wrap < end); - } + assert!(begin < disable_wrap); + assert!(disable_wrap < enable_wrap); + assert!(enable_wrap < end); + } - #[test] - fn trailing_empty_panes_do_not_trigger_scrolling() { - let mut terminal = Terminal::new((0, 9)); - let panes: Vec> = - vec![vec![StyledGraphemes::from("only row")], Vec::new()]; + #[test] + fn trailing_empty_panes_do_not_trigger_scrolling() { + let mut terminal = Terminal::new((0, 9)); + let panes: Vec> = + vec![vec![StyledGraphemes::from("only row")], Vec::new()]; - terminal.draw_rows_to(&mut Vec::new(), &panes, 10).unwrap(); + terminal.draw_rows_to(&mut Vec::new(), &panes, 10).unwrap(); - assert_eq!(terminal.position, (0, 9)); + assert_eq!(terminal.position, (0, 9)); + } + } } } diff --git a/promkit-core/src/widget.rs b/promkit-core/src/widget.rs index 4da3a750..7d17d4f3 100644 --- a/promkit-core/src/widget.rs +++ b/promkit-core/src/widget.rs @@ -161,39 +161,47 @@ pub trait Widget { mod tests { use super::*; - #[test] - fn viewport_does_not_scroll_while_position_is_visible() { - let mut viewport = WidgetViewport { - height: 3, - content_row: 4, - ..Default::default() - }; - - assert_eq!( - viewport.scroll_to_include(VisualPosition { row: 6, column: 0 }), - ViewportChange::Unchanged - ); - assert_eq!(viewport.content_row, 4); - } - - #[test] - fn viewport_scrolls_the_minimum_distance() { - let mut viewport = WidgetViewport { - height: 3, - content_row: 4, - ..Default::default() - }; - - assert_eq!( - viewport.scroll_to_include(VisualPosition { row: 7, column: 0 }), - ViewportChange::Scrolled - ); - assert_eq!(viewport.content_row, 5); - - assert_eq!( - viewport.scroll_to_include(VisualPosition { row: 2, column: 0 }), - ViewportChange::Scrolled - ); - assert_eq!(viewport.content_row, 2); + mod widget_viewport { + use super::*; + + mod scroll_to_include { + use super::*; + + #[test] + fn does_not_scroll_while_the_position_is_visible() { + let mut viewport = WidgetViewport { + height: 3, + content_row: 4, + ..Default::default() + }; + + assert_eq!( + viewport.scroll_to_include(VisualPosition { row: 6, column: 0 }), + ViewportChange::Unchanged + ); + assert_eq!(viewport.content_row, 4); + } + + #[test] + fn scrolls_the_minimum_distance_to_include_the_position() { + let mut viewport = WidgetViewport { + height: 3, + content_row: 4, + ..Default::default() + }; + + assert_eq!( + viewport.scroll_to_include(VisualPosition { row: 7, column: 0 }), + ViewportChange::Scrolled + ); + assert_eq!(viewport.content_row, 5); + + assert_eq!( + viewport.scroll_to_include(VisualPosition { row: 2, column: 0 }), + ViewportChange::Scrolled + ); + assert_eq!(viewport.content_row, 2); + } + } } } diff --git a/promkit-widgets/src/checkbox.rs b/promkit-widgets/src/checkbox.rs index ef36af4d..a7ae0f4a 100644 --- a/promkit-widgets/src/checkbox.rs +++ b/promkit-widgets/src/checkbox.rs @@ -92,17 +92,25 @@ pub enum CheckboxHit { mod tests { use super::*; - #[test] - fn resolves_item_rows_for_hits() { - let state = State { - checkbox: Checkbox::from_displayable(["first", "second"]), - config: Config::default(), - }; + mod state { + use super::*; + + mod hit_at { + use super::*; + + #[test] + fn resolves_item_rows() { + let state = State { + checkbox: Checkbox::from_displayable(["first", "second"]), + config: Config::default(), + }; - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 20 }), - Some(CheckboxHit::Toggle { index: 1 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 20 }), + Some(CheckboxHit::Toggle { index: 1 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + } + } } } diff --git a/promkit-widgets/src/checkbox/checkbox.rs b/promkit-widgets/src/checkbox/checkbox.rs index 4666d019..53aad5fe 100644 --- a/promkit-widgets/src/checkbox/checkbox.rs +++ b/promkit-widgets/src/checkbox/checkbox.rs @@ -141,19 +141,23 @@ impl Checkbox { mod tests { use super::*; - #[test] - fn toggling_an_empty_checkbox_is_a_no_op() { - let mut checkbox = Checkbox::from_displayable(Vec::::new()); - checkbox.toggle(); + mod toggle { + use super::*; + + #[test] + fn empty_checkbox_is_unchanged() { + let mut checkbox = Checkbox::from_displayable(Vec::::new()); + checkbox.toggle(); - assert!(checkbox.picked_indexes().is_empty()); + assert!(checkbox.picked_indexes().is_empty()); + } } mod new_with_checked { use super::*; #[test] - fn test() { + fn initializes_items_and_checked_indexes() { // Prepare a list of items with their checked status let items = vec![ (String::from("1"), true), diff --git a/promkit-widgets/src/checkbox/config.rs b/promkit-widgets/src/checkbox/config.rs index b7c4b6aa..948f14bc 100644 --- a/promkit-widgets/src/checkbox/config.rs +++ b/promkit-widgets/src/checkbox/config.rs @@ -39,13 +39,13 @@ impl Default for Config { #[cfg(test)] mod tests { #[cfg(feature = "serde")] - mod serde_compatibility { + mod deserialize { use promkit_core::crossterm::style::{Attribute, Color}; use super::super::Config; #[test] - fn config_fields_are_fully_loaded_from_toml() { + fn loads_all_fields_from_toml() { let input = r#" cursor = "> " active_mark = "*" diff --git a/promkit-widgets/src/listbox.rs b/promkit-widgets/src/listbox.rs index 7b1b0ee2..d6706335 100644 --- a/promkit-widgets/src/listbox.rs +++ b/promkit-widgets/src/listbox.rs @@ -84,17 +84,25 @@ pub enum ListboxHit { mod tests { use super::*; - #[test] - fn resolves_item_rows_for_hits() { - let state = State { - listbox: Listbox::from(["first", "second"]), - config: Config::default(), - }; + mod state { + use super::*; - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 20 }), - Some(ListboxHit::Select { index: 1 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + mod hit_at { + use super::*; + + #[test] + fn resolves_item_rows() { + let state = State { + listbox: Listbox::from(["first", "second"]), + config: Config::default(), + }; + + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 20 }), + Some(ListboxHit::Select { index: 1 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + } + } } } diff --git a/promkit-widgets/src/listbox/config.rs b/promkit-widgets/src/listbox/config.rs index eff601b4..26bcbccc 100644 --- a/promkit-widgets/src/listbox/config.rs +++ b/promkit-widgets/src/listbox/config.rs @@ -32,13 +32,13 @@ impl Default for Config { #[cfg(test)] mod tests { #[cfg(feature = "serde")] - mod serde_compatibility { + mod deserialize { use promkit_core::crossterm::style::{Attribute, Color}; use super::super::Config; #[test] - fn config_fields_are_fully_loaded_from_toml() { + fn loads_all_fields_from_toml() { let input = r#" cursor = "> " active_item_style = "fg=cyan,attr=bold" diff --git a/promkit-widgets/src/listbox/listbox.rs b/promkit-widgets/src/listbox/listbox.rs index 4a0c3209..e1b3f4e9 100644 --- a/promkit-widgets/src/listbox/listbox.rs +++ b/promkit-widgets/src/listbox/listbox.rs @@ -129,43 +129,80 @@ mod tests { use super::Listbox; - #[test] - fn default_is_empty() { - let listbox = Listbox::default(); - assert!(listbox.is_empty()); - assert_eq!(listbox.len(), 0); - assert_eq!(listbox.selected(), None); - assert!(!listbox.is_tail()); - } - - #[test] - fn pushing_first_item_selects_it() { - let mut listbox = Listbox::default(); - listbox.push_string("first".into()); - - assert_eq!(listbox.selected(), Some(0)); - assert_eq!(listbox.get(), StyledGraphemes::from("first")); - } - - #[test] - fn navigation_stops_at_the_list_boundaries() { - let mut listbox = Listbox::from(["first", "second"]); - - assert_eq!(listbox.selected(), Some(0)); - assert!(!listbox.backward()); - assert!(listbox.forward()); - assert_eq!(listbox.selected(), Some(1)); - assert!(!listbox.forward()); - - assert!(listbox.move_to(0)); - assert_eq!(listbox.selected(), Some(0)); - assert!(!listbox.move_to(2)); - assert_eq!(listbox.selected(), Some(0)); - - listbox.move_to_head(); - assert_eq!(listbox.selected(), Some(0)); - listbox.move_to_tail(); - assert_eq!(listbox.selected(), Some(1)); - assert!(listbox.is_tail()); + mod default { + use super::*; + + #[test] + fn creates_an_empty_list() { + let listbox = Listbox::default(); + assert!(listbox.is_empty()); + assert_eq!(listbox.len(), 0); + assert_eq!(listbox.selected(), None); + assert!(!listbox.is_tail()); + } + } + + mod push_string { + use super::*; + + #[test] + fn selects_the_first_item() { + let mut listbox = Listbox::default(); + listbox.push_string("first".into()); + + assert_eq!(listbox.selected(), Some(0)); + assert_eq!(listbox.get(), StyledGraphemes::from("first")); + } + } + + mod backward { + use super::*; + + #[test] + fn stops_at_the_head() { + let mut listbox = Listbox::from(["first", "second"]); + + assert!(!listbox.backward()); + assert_eq!(listbox.selected(), Some(0)); + } + } + + mod forward { + use super::*; + + #[test] + fn stops_at_the_tail() { + let mut listbox = Listbox::from(["first", "second"]); + + assert!(listbox.forward()); + assert_eq!(listbox.selected(), Some(1)); + assert!(!listbox.forward()); + assert_eq!(listbox.selected(), Some(1)); + } + } + + mod move_to { + use super::*; + + #[test] + fn rejects_an_out_of_bounds_index() { + let mut listbox = Listbox::from(["first", "second"]); + + assert!(!listbox.move_to(2)); + assert_eq!(listbox.selected(), Some(0)); + } + } + + mod move_to_tail { + use super::*; + + #[test] + fn selects_the_last_item() { + let mut listbox = Listbox::from(["first", "second"]); + + listbox.move_to_tail(); + assert_eq!(listbox.selected(), Some(1)); + assert!(listbox.is_tail()); + } } } diff --git a/promkit-widgets/src/prefix_search.rs b/promkit-widgets/src/prefix_search.rs index 1547e415..ec33fba6 100644 --- a/promkit-widgets/src/prefix_search.rs +++ b/promkit-widgets/src/prefix_search.rs @@ -87,41 +87,49 @@ mod tests { use super::{Config, PrefixSearch, PrefixSearchHit, State}; - #[test] - fn projects_trie_matches_and_resolves_hits() { - let mut prefix_search: PrefixSearch = ["apple", "applet", "application", "banana"] - .into_iter() - .collect(); - prefix_search.search("app"); - let mut state = State { - prefix_search, - config: Config { - lines: Some(3), - ..Default::default() - }, - }; + mod state { + use super::*; + + mod create_graphemes { + use super::*; - let created = state.create_graphemes(); + #[test] + fn projects_trie_matches_and_tracks_hit_rows() { + let mut prefix_search: PrefixSearch = ["apple", "applet", "application", "banana"] + .into_iter() + .collect(); + prefix_search.search("app"); + let mut state = State { + prefix_search, + config: Config { + lines: Some(3), + ..Default::default() + }, + }; - assert_eq!( - created.graphemes.to_string(), - "❯ apple\n applet\n application" - ); - assert_eq!(created.layout.max_height, Some(3)); - assert_eq!(created.cursor, Some(ContentPosition { row: 0, column: 0 })); - assert_eq!( - state.hit_at(ContentPosition { row: 2, column: 80 }), - Some(PrefixSearchHit::Select { index: 2 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 3, column: 0 }), None); + let created = state.create_graphemes(); - state.prefix_search.move_to(1); - let created = state.create_graphemes(); + assert_eq!( + created.graphemes.to_string(), + "❯ apple\n applet\n application" + ); + assert_eq!(created.layout.max_height, Some(3)); + assert_eq!(created.cursor, Some(ContentPosition { row: 0, column: 0 })); + assert_eq!( + state.hit_at(ContentPosition { row: 2, column: 80 }), + Some(PrefixSearchHit::Select { index: 2 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 3, column: 0 }), None); - assert_eq!( - created.graphemes.to_string(), - " apple\n❯ applet\n application" - ); - assert_eq!(created.cursor, Some(ContentPosition { row: 1, column: 0 })); + state.prefix_search.move_to(1); + let created = state.create_graphemes(); + + assert_eq!( + created.graphemes.to_string(), + " apple\n❯ applet\n application" + ); + assert_eq!(created.cursor, Some(ContentPosition { row: 1, column: 0 })); + } + } } } diff --git a/promkit-widgets/src/prefix_search/config.rs b/promkit-widgets/src/prefix_search/config.rs index 82f2b3b7..c5e91eaf 100644 --- a/promkit-widgets/src/prefix_search/config.rs +++ b/promkit-widgets/src/prefix_search/config.rs @@ -32,13 +32,13 @@ impl Default for Config { #[cfg(test)] mod tests { #[cfg(feature = "serde")] - mod serde_compatibility { + mod deserialize { use promkit_core::crossterm::style::{Attribute, Color}; use super::super::Config; #[test] - fn config_fields_are_fully_loaded_from_toml() { + fn loads_all_fields_from_toml() { let input = r#" cursor = "> " active_item_style = "fg=cyan,attr=bold" diff --git a/promkit-widgets/src/prefix_search/prefix_search.rs b/promkit-widgets/src/prefix_search/prefix_search.rs index 28c46641..fba29407 100644 --- a/promkit-widgets/src/prefix_search/prefix_search.rs +++ b/promkit-widgets/src/prefix_search/prefix_search.rs @@ -118,65 +118,109 @@ mod tests { .collect() } - #[test] - fn search_filters_trie_and_selects_first_match() { - let mut prefix_search = prefix_search(); - - assert!(prefix_search.search("app")); - assert_eq!( - prefix_search.candidates().collect::>(), - vec!["apple", "applet", "application"] - ); - assert_eq!(prefix_search.selected(), Some(0)); - assert_eq!(prefix_search.get(), Some("apple")); - - assert!(prefix_search.search("ban")); - assert_eq!( - prefix_search.candidates().collect::>(), - vec!["banana"] - ); - assert_eq!(prefix_search.selected(), Some(0)); - assert_eq!(prefix_search.get(), Some("banana")); - } - - #[test] - fn search_without_a_match_clears_the_selection() { - let mut prefix_search = prefix_search(); - - assert!(prefix_search.search("app")); - assert!(!prefix_search.search("orange")); - assert!(prefix_search.is_empty()); - assert_eq!(prefix_search.selected(), None); - assert_eq!(prefix_search.get(), None); - } - - #[test] - fn navigation_stops_at_match_boundaries() { - let mut prefix_search = prefix_search(); - prefix_search.search("app"); - - assert!(!prefix_search.backward()); - assert!(prefix_search.forward()); - assert_eq!(prefix_search.get(), Some("applet")); - assert!(prefix_search.forward()); - assert_eq!(prefix_search.get(), Some("application")); - assert!(!prefix_search.forward()); - assert!(!prefix_search.move_to(3)); - assert_eq!(prefix_search.selected(), Some(2)); - assert!(prefix_search.move_to(0)); - assert_eq!(prefix_search.get(), Some("apple")); - } - - #[test] - fn clear_hides_matches_without_removing_candidates() { - let mut prefix_search = prefix_search(); - prefix_search.search("app"); - - prefix_search.clear(); - - assert!(prefix_search.is_empty()); - assert_eq!(prefix_search.selected(), None); - assert!(prefix_search.search("app")); - assert_eq!(prefix_search.get(), Some("apple")); + mod search { + use super::*; + + #[test] + fn filters_the_trie_and_selects_the_first_match() { + let mut prefix_search = prefix_search(); + + assert!(prefix_search.search("app")); + assert_eq!( + prefix_search.candidates().collect::>(), + vec!["apple", "applet", "application"] + ); + assert_eq!(prefix_search.selected(), Some(0)); + assert_eq!(prefix_search.get(), Some("apple")); + + assert!(prefix_search.search("ban")); + assert_eq!( + prefix_search.candidates().collect::>(), + vec!["banana"] + ); + assert_eq!(prefix_search.selected(), Some(0)); + assert_eq!(prefix_search.get(), Some("banana")); + } + + #[test] + fn missing_match_clears_the_selection() { + let mut prefix_search = prefix_search(); + + assert!(prefix_search.search("app")); + assert!(!prefix_search.search("orange")); + assert!(prefix_search.is_empty()); + assert_eq!(prefix_search.selected(), None); + assert_eq!(prefix_search.get(), None); + } + } + + mod backward { + use super::*; + + #[test] + fn stops_at_the_first_match() { + let mut prefix_search = prefix_search(); + prefix_search.search("app"); + + assert!(!prefix_search.backward()); + assert_eq!(prefix_search.get(), Some("apple")); + } + } + + mod forward { + use super::*; + + #[test] + fn stops_at_the_last_match() { + let mut prefix_search = prefix_search(); + prefix_search.search("app"); + + assert!(prefix_search.forward()); + assert_eq!(prefix_search.get(), Some("applet")); + assert!(prefix_search.forward()); + assert_eq!(prefix_search.get(), Some("application")); + assert!(!prefix_search.forward()); + } + } + + mod move_to { + use super::*; + + #[test] + fn rejects_an_out_of_bounds_index() { + let mut prefix_search = prefix_search(); + prefix_search.search("app"); + prefix_search.move_to(2); + + assert!(!prefix_search.move_to(3)); + assert_eq!(prefix_search.selected(), Some(2)); + } + + #[test] + fn selects_the_given_match() { + let mut prefix_search = prefix_search(); + prefix_search.search("app"); + prefix_search.move_to(2); + + assert!(prefix_search.move_to(0)); + assert_eq!(prefix_search.get(), Some("apple")); + } + } + + mod clear { + use super::*; + + #[test] + fn hides_matches_without_removing_candidates() { + let mut prefix_search = prefix_search(); + prefix_search.search("app"); + + prefix_search.clear(); + + assert!(prefix_search.is_empty()); + assert_eq!(prefix_search.selected(), None); + assert!(prefix_search.search("app")); + assert_eq!(prefix_search.get(), Some("apple")); + } } } diff --git a/promkit-widgets/src/structured/json.rs b/promkit-widgets/src/structured/json.rs index a181fbf8..925798c1 100644 --- a/promkit-widgets/src/structured/json.rs +++ b/promkit-widgets/src/structured/json.rs @@ -114,166 +114,178 @@ pub enum JsonHit { mod tests { use super::*; - #[test] - fn viewport_projection_is_bounded_and_resolves_hits() { - let value = serde_json::json!({ - "first": "one", - "second": { - "nested": "two" - }, - "third": "three" - }); - let mut state = State { - document: Document::new([&value]), - config: Config::default(), - }; - - state.create_graphemes_in_viewport(80, 2); - state.document.down(); - state.create_graphemes_in_viewport(80, 2); - state.document.down(); - let projected = state.create_graphemes_in_viewport(80, 2); - let rendered = projected.graphemes.to_string(); - - assert!(rendered.contains("\"first\": \"one\"")); - assert!(rendered.contains("\"second\": {")); - assert!(!rendered.contains("\"nested\": \"two\"")); - assert!(!rendered.contains("\"third\": \"three\"")); - assert_eq!(projected.cursor.unwrap().row, 1); - - let JsonHit::Toggle { row_index } = state - .hit_at_viewport(ContentPosition { row: 1, column: 4 }) - .unwrap(); - state.document.toggle_at(row_index); - - let collapsed = state.create_graphemes_in_viewport(80, 2); - assert!(collapsed.graphemes.to_string().contains("\"second\": {…}")); - } - - #[test] - fn viewport_projection_stays_stable_until_cursor_leaves_it() { - let value = serde_json::json!({ - "first": 1, - "second": 2, - "third": 3, - "fourth": 4 - }); - let mut state = State { - document: Document::new([&value]), - config: Config::default(), - }; - - let initial = state.create_graphemes_in_viewport(80, 3); - assert!(initial.graphemes.to_string().starts_with("{")); - assert_eq!(initial.cursor.unwrap().row, 0); - - state.document.down(); - let moved_inside = state.create_graphemes_in_viewport(80, 3); - assert!(moved_inside.graphemes.to_string().starts_with("{")); - assert_eq!(moved_inside.cursor.unwrap().row, 1); - - state.document.down(); - let moved_to_edge = state.create_graphemes_in_viewport(80, 3); - assert!(moved_to_edge.graphemes.to_string().starts_with("{")); - assert_eq!(moved_to_edge.cursor.unwrap().row, 2); - - state.document.down(); - let moved_outside = state.create_graphemes_in_viewport(80, 3); - assert!( - moved_outside - .graphemes - .to_string() - .starts_with("\"first\": 1") - ); - assert_eq!(moved_outside.cursor.unwrap().row, 2); - - state.document.up(); - let moved_back_inside = state.create_graphemes_in_viewport(80, 3); - assert!( - moved_back_inside - .graphemes - .to_string() - .starts_with("\"first\": 1") - ); - assert_eq!(moved_back_inside.cursor.unwrap().row, 1); - - state.document.up(); - state.create_graphemes_in_viewport(80, 3); - state.document.up(); - let moved_above = state.create_graphemes_in_viewport(80, 3); - assert!(moved_above.graphemes.to_string().starts_with("{")); - assert_eq!(moved_above.cursor.unwrap().row, 0); - } - - #[test] - fn configured_line_limit_bounds_viewport_projection() { - let value = serde_json::json!({"first": 1, "second": 2}); - let state = State { - document: Document::new([&value]), - config: Config { - lines: Some(1), - ..Config::default() - }, - }; - - let projected = state.create_graphemes_in_viewport(80, 20); - assert_eq!(projected.graphemes.logical_lines().len(), 1); - } - - #[test] - fn preserves_expanded_line_numbers_after_toggle() { - let value = serde_json::json!({ - "first": { - "nested": 1 - }, - "last": 2 - }); - let mut state = State { - document: Document::new([&value]), - config: Config { - indent: 2, - show_line_numbers: true, - ..Default::default() - }, - }; - - assert_eq!( - state.document.visible_line_numbers(), - vec![1, 2, 3, 4, 5, 6] - ); - - state.document.down(); - state.document.toggle(); - - assert_eq!(state.document.visible_line_numbers(), vec![1, 2, 5, 6]); - assert_eq!( - state.create_graphemes().graphemes.to_string(), - "1 {\n2 \"first\": {…},\n5 \"last\": 2\n6 }" - ); - } + mod state { + use super::*; + + mod create_graphemes_in_viewport { + use super::*; + + #[test] + fn viewport_projection_is_bounded_and_resolves_hits() { + let value = serde_json::json!({ + "first": "one", + "second": { + "nested": "two" + }, + "third": "three" + }); + let mut state = State { + document: Document::new([&value]), + config: Config::default(), + }; + + state.create_graphemes_in_viewport(80, 2); + state.document.down(); + state.create_graphemes_in_viewport(80, 2); + state.document.down(); + let projected = state.create_graphemes_in_viewport(80, 2); + let rendered = projected.graphemes.to_string(); + + assert!(rendered.contains("\"first\": \"one\"")); + assert!(rendered.contains("\"second\": {")); + assert!(!rendered.contains("\"nested\": \"two\"")); + assert!(!rendered.contains("\"third\": \"three\"")); + assert_eq!(projected.cursor.unwrap().row, 1); + + let JsonHit::Toggle { row_index } = state + .hit_at_viewport(ContentPosition { row: 1, column: 4 }) + .unwrap(); + state.document.toggle_at(row_index); + + let collapsed = state.create_graphemes_in_viewport(80, 2); + assert!(collapsed.graphemes.to_string().contains("\"second\": {…}")); + } + + #[test] + fn viewport_projection_stays_stable_until_cursor_leaves_it() { + let value = serde_json::json!({ + "first": 1, + "second": 2, + "third": 3, + "fourth": 4 + }); + let mut state = State { + document: Document::new([&value]), + config: Config::default(), + }; + + let initial = state.create_graphemes_in_viewport(80, 3); + assert!(initial.graphemes.to_string().starts_with("{")); + assert_eq!(initial.cursor.unwrap().row, 0); + + state.document.down(); + let moved_inside = state.create_graphemes_in_viewport(80, 3); + assert!(moved_inside.graphemes.to_string().starts_with("{")); + assert_eq!(moved_inside.cursor.unwrap().row, 1); + + state.document.down(); + let moved_to_edge = state.create_graphemes_in_viewport(80, 3); + assert!(moved_to_edge.graphemes.to_string().starts_with("{")); + assert_eq!(moved_to_edge.cursor.unwrap().row, 2); + + state.document.down(); + let moved_outside = state.create_graphemes_in_viewport(80, 3); + assert!( + moved_outside + .graphemes + .to_string() + .starts_with("\"first\": 1") + ); + assert_eq!(moved_outside.cursor.unwrap().row, 2); + + state.document.up(); + let moved_back_inside = state.create_graphemes_in_viewport(80, 3); + assert!( + moved_back_inside + .graphemes + .to_string() + .starts_with("\"first\": 1") + ); + assert_eq!(moved_back_inside.cursor.unwrap().row, 1); + + state.document.up(); + state.create_graphemes_in_viewport(80, 3); + state.document.up(); + let moved_above = state.create_graphemes_in_viewport(80, 3); + assert!(moved_above.graphemes.to_string().starts_with("{")); + assert_eq!(moved_above.cursor.unwrap().row, 0); + } + + #[test] + fn configured_line_limit_bounds_viewport_projection() { + let value = serde_json::json!({"first": 1, "second": 2}); + let state = State { + document: Document::new([&value]), + config: Config { + lines: Some(1), + ..Config::default() + }, + }; + + let projected = state.create_graphemes_in_viewport(80, 20); + assert_eq!(projected.graphemes.logical_lines().len(), 1); + } + + #[test] + fn viewport_projection_uses_stable_line_numbers() { + let value = serde_json::json!({"first": 1, "second": 2, "third": 3}); + let mut state = State { + document: Document::new([&value]), + config: Config { + show_line_numbers: true, + ..Default::default() + }, + }; + + state.create_graphemes_in_viewport(80, 2); + state.document.down(); + state.document.down(); + state.document.down(); + + assert_eq!( + state + .create_graphemes_in_viewport(80, 2) + .graphemes + .to_string(), + "3 \"second\": 2,\n4 \"third\": 3" + ); + } + } - #[test] - fn viewport_projection_uses_stable_line_numbers() { - let value = serde_json::json!({"first": 1, "second": 2, "third": 3}); - let mut state = State { - document: Document::new([&value]), - config: Config { - show_line_numbers: true, - ..Default::default() - }, - }; - - state.create_graphemes_in_viewport(80, 2); - state.document.down(); - state.document.down(); - state.document.down(); - - assert_eq!( - state - .create_graphemes_in_viewport(80, 2) - .graphemes - .to_string(), - "3 \"second\": 2,\n4 \"third\": 3" - ); + mod create_graphemes { + use super::*; + + #[test] + fn preserves_expanded_line_numbers_after_toggle() { + let value = serde_json::json!({ + "first": { + "nested": 1 + }, + "last": 2 + }); + let mut state = State { + document: Document::new([&value]), + config: Config { + indent: 2, + show_line_numbers: true, + ..Default::default() + }, + }; + + assert_eq!( + state.document.visible_line_numbers(), + vec![1, 2, 3, 4, 5, 6] + ); + + state.document.down(); + state.document.toggle(); + + assert_eq!(state.document.visible_line_numbers(), vec![1, 2, 5, 6]); + assert_eq!( + state.create_graphemes().graphemes.to_string(), + "1 {\n2 \"first\": {…},\n5 \"last\": 2\n6 }" + ); + } + } } } diff --git a/promkit-widgets/src/structured/json/config.rs b/promkit-widgets/src/structured/json/config.rs index ec37b40c..b8691510 100644 --- a/promkit-widgets/src/structured/json/config.rs +++ b/promkit-widgets/src/structured/json/config.rs @@ -261,92 +261,95 @@ mod tests { use super::*; use serde_json::json; - mod render_terminal_rows { + mod config { use super::*; - use crate::structured::json::jsonz::create_rows; + mod render_terminal_rows { + use super::*; - #[test] - fn test_ellipsis_mode_truncates_with_ellipsis() { - let value = json!({ - "very_long_key": "abcdefghijklmnopqrstuvwxyz", - }); - let rows = create_rows([&value]); - let width = 12; + use crate::structured::json::jsonz::create_rows; - let lines = Config { - indent: 2, - overflow_mode: OverflowMode::Truncate, - ..Default::default() - } - .render_terminal_rows(&rows, width); - - assert_eq!(lines.len(), rows.len()); - assert!(lines.iter().all(|line| line.widths() <= width as usize)); - assert!( - lines - .iter() - .any(|line| line.chars().last().is_some_and(|ch| *ch == '…')) - ); - } + #[test] + fn truncate_mode_appends_an_ellipsis() { + let value = json!({ + "very_long_key": "abcdefghijklmnopqrstuvwxyz", + }); + let rows = create_rows([&value]); + let width = 12; - #[test] - fn test_linewrap_mode_wraps_without_ellipsis() { - let value = json!({ - "very_long_key": "abcdefghijklmnopqrstuvwxyz", - }); - let rows = create_rows([&value]); - let width = 12; + let lines = Config { + indent: 2, + overflow_mode: OverflowMode::Truncate, + ..Default::default() + } + .render_terminal_rows(&rows, width); + + assert_eq!(lines.len(), rows.len()); + assert!(lines.iter().all(|line| line.widths() <= width as usize)); + assert!( + lines + .iter() + .any(|line| line.chars().last().is_some_and(|ch| *ch == '…')) + ); + } - let lines = Config { - indent: 2, - overflow_mode: OverflowMode::Wrap, - ..Default::default() + #[test] + fn wrap_mode_wraps_without_an_ellipsis() { + let value = json!({ + "very_long_key": "abcdefghijklmnopqrstuvwxyz", + }); + let rows = create_rows([&value]); + let width = 12; + + let lines = Config { + indent: 2, + overflow_mode: OverflowMode::Wrap, + ..Default::default() + } + .render_terminal_rows(&rows, width); + + assert!(lines.len() > rows.len()); + assert!(lines.iter().all(|line| line.widths() <= width as usize)); + assert!( + lines + .iter() + .all(|line| !matches!(line.chars().last(), Some('…'))) + ); } - .render_terminal_rows(&rows, width); - - assert!(lines.len() > rows.len()); - assert!(lines.iter().all(|line| line.widths() <= width as usize)); - assert!( - lines - .iter() - .all(|line| !matches!(line.chars().last(), Some('…'))) - ); } - } - #[cfg(feature = "serde")] - mod serde_compatibility { - use super::*; - use promkit_core::crossterm::style::{Attributes, Color}; - - #[test] - fn missing_new_fields_are_filled_by_default() { - let mut value = serde_json::to_value(Config { - indent: 4, - ..Default::default() - }) - .unwrap(); - let obj = value.as_object_mut().unwrap(); - obj.remove("active_item_attribute"); - obj.remove("inactive_item_attribute"); - obj.remove("overflow_mode"); - obj.remove("lines"); - obj.remove("show_line_numbers"); - - let formatter: Config = serde_json::from_value(value).unwrap(); - - assert_eq!(formatter.indent, 4); - assert_eq!(formatter.active_item_attribute, Attribute::NoBold); - assert_eq!(formatter.inactive_item_attribute, Attribute::NoBold); - assert_eq!(formatter.overflow_mode, OverflowMode::Truncate); - assert_eq!(formatter.lines, None); - assert!(!formatter.show_line_numbers); - } + #[cfg(feature = "serde")] + mod deserialize { + use super::*; + use promkit_core::crossterm::style::{Attributes, Color}; + + #[test] + fn missing_new_fields_are_filled_by_default() { + let mut value = serde_json::to_value(Config { + indent: 4, + ..Default::default() + }) + .unwrap(); + let obj = value.as_object_mut().unwrap(); + obj.remove("active_item_attribute"); + obj.remove("inactive_item_attribute"); + obj.remove("overflow_mode"); + obj.remove("lines"); + obj.remove("show_line_numbers"); + + let formatter: Config = serde_json::from_value(value).unwrap(); + + assert_eq!(formatter.indent, 4); + assert_eq!(formatter.active_item_attribute, Attribute::NoBold); + assert_eq!(formatter.inactive_item_attribute, Attribute::NoBold); + assert_eq!(formatter.overflow_mode, OverflowMode::Truncate); + assert_eq!(formatter.lines, None); + assert!(!formatter.show_line_numbers); + } - #[test] - fn config_fields_are_fully_loaded_from_toml() { - let input = r#" + #[test] + fn loads_all_fields_from_toml() { + let input = r#" indent = 4 lines = 7 show_line_numbers = true @@ -362,39 +365,40 @@ mod tests { overflow_mode = "Wrap" "#; - let formatter: Config = toml::from_str(input).unwrap(); - - assert_eq!(formatter.indent, 4); - assert_eq!(formatter.lines, Some(7)); - assert!(formatter.show_line_numbers); - assert_eq!( - formatter.curly_brackets_style.attributes, - Attributes::from(Attribute::Bold), - ); - assert_eq!( - formatter.square_brackets_style.attributes, - Attributes::from(Attribute::Bold), - ); - assert_eq!(formatter.key_style.foreground_color, Some(Color::Cyan)); - assert_eq!( - formatter.string_value_style.foreground_color, - Some(Color::Green), - ); - assert_eq!( - formatter.number_value_style.foreground_color, - Some(Color::Yellow) - ); - assert_eq!( - formatter.boolean_value_style.foreground_color, - Some(Color::Magenta), - ); - assert_eq!( - formatter.null_value_style.foreground_color, - Some(Color::Grey) - ); - assert_eq!(formatter.active_item_attribute, Attribute::Underlined); - assert_eq!(formatter.inactive_item_attribute, Attribute::Dim); - assert_eq!(formatter.overflow_mode, OverflowMode::Wrap); + let formatter: Config = toml::from_str(input).unwrap(); + + assert_eq!(formatter.indent, 4); + assert_eq!(formatter.lines, Some(7)); + assert!(formatter.show_line_numbers); + assert_eq!( + formatter.curly_brackets_style.attributes, + Attributes::from(Attribute::Bold), + ); + assert_eq!( + formatter.square_brackets_style.attributes, + Attributes::from(Attribute::Bold), + ); + assert_eq!(formatter.key_style.foreground_color, Some(Color::Cyan)); + assert_eq!( + formatter.string_value_style.foreground_color, + Some(Color::Green), + ); + assert_eq!( + formatter.number_value_style.foreground_color, + Some(Color::Yellow) + ); + assert_eq!( + formatter.boolean_value_style.foreground_color, + Some(Color::Magenta), + ); + assert_eq!( + formatter.null_value_style.foreground_color, + Some(Color::Grey) + ); + assert_eq!(formatter.active_item_attribute, Attribute::Underlined); + assert_eq!(formatter.inactive_item_attribute, Attribute::Dim); + assert_eq!(formatter.overflow_mode, OverflowMode::Wrap); + } } } } diff --git a/promkit-widgets/src/structured/json/document.rs b/promkit-widgets/src/structured/json/document.rs index a1e01ced..f89cda8f 100644 --- a/promkit-widgets/src/structured/json/document.rs +++ b/promkit-widgets/src/structured/json/document.rs @@ -225,43 +225,55 @@ mod tests { Document::new(values.iter()) } - #[test] - fn from_str_matches_value_conversion() { - let expected = via_value(INPUT); - let actual = Document::from_str(INPUT).unwrap(); + mod from_str { + use super::*; - assert_eq!(actual.rows(), expected.rows()); - } + #[test] + fn matches_value_conversion() { + let expected = via_value(INPUT); + let actual = Document::from_str(INPUT).unwrap(); - #[test] - fn from_reader_matches_value_conversion() { - let expected = via_value(INPUT); - let actual = Document::from_reader(Cursor::new(INPUT.as_bytes())).unwrap(); + assert_eq!(actual.rows(), expected.rows()); + } - assert_eq!(actual.rows(), expected.rows()); - } + #[test] + fn matches_value_conversion_for_edge_cases() { + for input in [ + "", + " \n\t", + "null", + "{}", + "[]", + "-0 1.25e3", + r#""escaped\ntext" {"a":[],"b":{}}"#, + ] { + let expected = via_value(input); + let actual = Document::from_str(input).unwrap(); + + assert_eq!(actual.rows(), expected.rows(), "input: {input:?}"); + } + } - #[test] - fn direct_parsing_matches_value_conversion_for_edge_cases() { - for input in [ - "", - " \n\t", - "null", - "{}", - "[]", - "-0 1.25e3", - r#""escaped\ntext" {"a":[],"b":{}}"#, - ] { - let expected = via_value(input); - let actual = Document::from_str(input).unwrap(); - - assert_eq!(actual.rows(), expected.rows(), "input: {input:?}"); + #[test] + fn reports_invalid_json() { + assert!(Document::from_str(r#"{"missing":"close""#).is_err()); } } - #[test] - fn direct_parsing_reports_invalid_json() { - assert!(Document::from_str(r#"{"missing":"close""#).is_err()); - assert!(Document::from_reader(Cursor::new(b"[1,")).is_err()); + mod from_reader { + use super::*; + + #[test] + fn matches_value_conversion() { + let expected = via_value(INPUT); + let actual = Document::from_reader(Cursor::new(INPUT.as_bytes())).unwrap(); + + assert_eq!(actual.rows(), expected.rows()); + } + + #[test] + fn reports_invalid_json() { + assert!(Document::from_reader(Cursor::new(b"[1,")).is_err()); + } } } diff --git a/promkit-widgets/src/structured/tree.rs b/promkit-widgets/src/structured/tree.rs index 5adbfee2..3d7a29db 100644 --- a/promkit-widgets/src/structured/tree.rs +++ b/promkit-widgets/src/structured/tree.rs @@ -100,75 +100,87 @@ pub enum TreeHit { mod tests { use super::*; - #[test] - fn resolves_visible_rows_for_hits() { - let state = State { - document: Document::new(vec![ - Row { - id: "root".into(), - path: vec!["root".into()], - depth: 0, - has_children: true, - collapsed: false, - }, - Row { - id: "child".into(), - path: vec!["root".into(), "child".into()], - depth: 1, - has_children: false, - collapsed: false, - }, - ]), - config: Config::default(), - }; + mod state { + use super::*; - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 20 }), - Some(TreeHit::Toggle { row_index: 1 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); - } + mod hit_at { + use super::*; - #[test] - fn preserves_expanded_line_numbers_after_toggle() { - let mut state = State { - document: Document::new(vec![ - Row { - id: "root".into(), - path: vec!["root".into()], - depth: 0, - has_children: true, - collapsed: false, - }, - Row { - id: "child".into(), - path: vec!["root".into(), "child".into()], - depth: 1, - has_children: false, - collapsed: false, - }, - Row { - id: "sibling".into(), - path: vec!["sibling".into()], - depth: 0, - has_children: false, - collapsed: false, - }, - ]), - config: Config { - show_line_numbers: true, - ..Default::default() - }, - }; + #[test] + fn resolves_visible_rows() { + let state = State { + document: Document::new(vec![ + Row { + id: "root".into(), + path: vec!["root".into()], + depth: 0, + has_children: true, + collapsed: false, + }, + Row { + id: "child".into(), + path: vec!["root".into(), "child".into()], + depth: 1, + has_children: false, + collapsed: false, + }, + ]), + config: Config::default(), + }; + + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 20 }), + Some(TreeHit::Toggle { row_index: 1 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + } + } + + mod create_graphemes { + use super::*; - assert_eq!(state.document.visible_line_numbers(), vec![1, 2, 3]); + #[test] + fn preserves_expanded_line_numbers_after_toggle() { + let mut state = State { + document: Document::new(vec![ + Row { + id: "root".into(), + path: vec!["root".into()], + depth: 0, + has_children: true, + collapsed: false, + }, + Row { + id: "child".into(), + path: vec!["root".into(), "child".into()], + depth: 1, + has_children: false, + collapsed: false, + }, + Row { + id: "sibling".into(), + path: vec!["sibling".into()], + depth: 0, + has_children: false, + collapsed: false, + }, + ]), + config: Config { + show_line_numbers: true, + ..Default::default() + }, + }; - state.document.toggle(); + assert_eq!(state.document.visible_line_numbers(), vec![1, 2, 3]); - assert_eq!(state.document.visible_line_numbers(), vec![1, 3]); - let rendered = state.create_graphemes().graphemes.to_string(); - assert!(rendered.starts_with("1 ")); - assert!(rendered.contains("\n3 ")); - assert!(!rendered.contains("\n2 ")); + state.document.toggle(); + + assert_eq!(state.document.visible_line_numbers(), vec![1, 3]); + let rendered = state.create_graphemes().graphemes.to_string(); + assert!(rendered.starts_with("1 ")); + assert!(rendered.contains("\n3 ")); + assert!(!rendered.contains("\n2 ")); + } + } } } diff --git a/promkit-widgets/src/structured/tree/config.rs b/promkit-widgets/src/structured/tree/config.rs index 5eb5f1f3..01bead8a 100644 --- a/promkit-widgets/src/structured/tree/config.rs +++ b/promkit-widgets/src/structured/tree/config.rs @@ -42,13 +42,13 @@ impl Default for Config { #[cfg(test)] mod tests { #[cfg(feature = "serde")] - mod serde_compatibility { + mod deserialize { use promkit_core::crossterm::style::{Attribute, Color}; use super::super::Config; #[test] - fn config_fields_are_fully_loaded_from_toml() { + fn loads_all_fields_from_toml() { let input = r#" folded_symbol = "> " unfolded_symbol = "v " diff --git a/promkit-widgets/src/structured/tree/treez.rs b/promkit-widgets/src/structured/tree/treez.rs index fcfe6e53..24c4cdd7 100644 --- a/promkit-widgets/src/structured/tree/treez.rs +++ b/promkit-widgets/src/structured/tree/treez.rs @@ -240,97 +240,117 @@ mod tests { ] } - #[test] - fn extract_skips_hidden_descendants() { - let mut rows = create_test_rows(); - rows[0].collapsed = true; - - assert_eq!( - rows.extract(0, 5), - vec![Row { - depth: 0, - id: "root".into(), - path: vec!["root".into()], - has_children: true, - collapsed: true, - }] - ); - } + mod row_operation { + use super::*; + + mod extract { + use super::*; + + #[test] + fn skips_hidden_descendants() { + let mut rows = create_test_rows(); + rows[0].collapsed = true; + + assert_eq!( + rows.extract(0, 5), + vec![Row { + depth: 0, + id: "root".into(), + path: vec!["root".into()], + has_children: true, + collapsed: true, + }] + ); + } + } + + mod down { + use super::*; - #[test] - fn down_skips_hidden_descendants() { - let mut rows = create_test_rows(); - rows[1].collapsed = true; + #[test] + fn skips_hidden_descendants() { + let mut rows = create_test_rows(); + rows[1].collapsed = true; - assert_eq!(rows.down(1), 4); + assert_eq!(rows.down(1), 4); + } + } } - #[test] - fn create_rows_is_generic() { - let root = TestNode { - id: "root", - children: vec![ - TestNode { - id: "a", + mod adapter { + use super::*; + + mod create_rows { + use super::*; + + #[test] + fn supports_arbitrary_node_types() { + let root = TestNode { + id: "root", children: vec![ TestNode { - id: "aa", - children: vec![], + id: "a", + children: vec![ + TestNode { + id: "aa", + children: vec![], + }, + TestNode { + id: "ab", + children: vec![], + }, + ], }, TestNode { - id: "ab", + id: "b", children: vec![], }, ], - }, - TestNode { - id: "b", - children: vec![], - }, - ], - }; - - let rows = TestAdapter.create_rows(&root).unwrap(); - - assert_eq!( - rows, - vec![ - Row { - depth: 0, - id: "root".into(), - path: vec!["root".into()], - has_children: true, - collapsed: true, - }, - Row { - depth: 1, - id: "a".into(), - path: vec!["root".into(), "a".into()], - has_children: true, - collapsed: true, - }, - Row { - depth: 2, - id: "aa".into(), - path: vec!["root".into(), "a".into(), "aa".into()], - has_children: false, - collapsed: false, - }, - Row { - depth: 2, - id: "ab".into(), - path: vec!["root".into(), "a".into(), "ab".into()], - has_children: false, - collapsed: false, - }, - Row { - depth: 1, - id: "b".into(), - path: vec!["root".into(), "b".into()], - has_children: false, - collapsed: false, - }, - ] - ); + }; + + let rows = TestAdapter.create_rows(&root).unwrap(); + + assert_eq!( + rows, + vec![ + Row { + depth: 0, + id: "root".into(), + path: vec!["root".into()], + has_children: true, + collapsed: true, + }, + Row { + depth: 1, + id: "a".into(), + path: vec!["root".into(), "a".into()], + has_children: true, + collapsed: true, + }, + Row { + depth: 2, + id: "aa".into(), + path: vec!["root".into(), "a".into(), "aa".into()], + has_children: false, + collapsed: false, + }, + Row { + depth: 2, + id: "ab".into(), + path: vec!["root".into(), "a".into(), "ab".into()], + has_children: false, + collapsed: false, + }, + Row { + depth: 1, + id: "b".into(), + path: vec!["root".into(), "b".into()], + has_children: false, + collapsed: false, + }, + ] + ); + } + } } } diff --git a/promkit-widgets/src/structured/yaml.rs b/promkit-widgets/src/structured/yaml.rs index 592b8991..e51bc5c2 100644 --- a/promkit-widgets/src/structured/yaml.rs +++ b/promkit-widgets/src/structured/yaml.rs @@ -104,187 +104,202 @@ pub enum YamlHit { mod tests { use super::*; - #[test] - fn creates_full_content_and_resolves_visible_rows_for_hits() { - let value = - serde_yaml::from_str("first: one\nsecond:\n nested: two\nthird: three\n").unwrap(); - let mut state = State { - document: Document::new([&value]), - config: Config::default(), - }; - - let initial = state.create_graphemes(); - assert!(initial.graphemes.to_string().contains("first: one")); - assert!(initial.graphemes.to_string().contains("third: three")); - assert_eq!(initial.cursor.unwrap().row, 0); - - state.document.down(); - let moved = state.create_graphemes(); - assert!(moved.graphemes.to_string().contains("first: one")); - assert_eq!(moved.cursor.unwrap().row, 1); - - assert!(matches!( - state.hit_at(ContentPosition { row: 1, column: 4 }), - Some(YamlHit::Toggle { .. }) - )); - } - - #[test] - fn viewport_projection_is_bounded_and_resolves_hits() { - let value = serde_yaml::from_str( - "first: one\nsecond:\n nested: two\n extra: value\nthird: three\n", - ) - .unwrap(); - let mut state = State { - document: Document::new([&value]), - config: Config::default(), - }; - - state.create_graphemes_in_viewport(80, 2); - state.document.down(); - let projected = state.create_graphemes_in_viewport(80, 2); - let rendered = projected.graphemes.to_string(); - - assert!(rendered.contains("first: one")); - assert!(rendered.contains("second: ")); - assert!(!rendered.contains("nested: two")); - assert!(!rendered.contains("extra: value")); - assert!(!rendered.contains("third: three")); - assert_eq!(projected.cursor.unwrap().row, 1); - - let YamlHit::Toggle { row_index } = state - .hit_at_viewport(ContentPosition { row: 1, column: 4 }) - .unwrap(); - state.document.toggle_at(row_index); - - let collapsed = state.create_graphemes_in_viewport(80, 2); - assert!(collapsed.graphemes.to_string().contains("second: {…}")); - } - - #[test] - fn viewport_projection_stays_stable_until_cursor_leaves_it() { - let value = serde_yaml::from_str("first: 1\nsecond: 2\nthird: 3\nfourth: 4\n").unwrap(); - let mut state = State { - document: Document::new([&value]), - config: Config::default(), - }; - - let initial = state.create_graphemes_in_viewport(80, 3); - assert!(initial.graphemes.to_string().starts_with("first: 1")); - assert_eq!(initial.cursor.unwrap().row, 0); - - state.document.down(); - let moved_inside = state.create_graphemes_in_viewport(80, 3); - assert!(moved_inside.graphemes.to_string().starts_with("first: 1")); - assert_eq!(moved_inside.cursor.unwrap().row, 1); - - state.document.down(); - let moved_to_edge = state.create_graphemes_in_viewport(80, 3); - assert!(moved_to_edge.graphemes.to_string().starts_with("first: 1")); - assert_eq!(moved_to_edge.cursor.unwrap().row, 2); - - state.document.down(); - let moved_outside = state.create_graphemes_in_viewport(80, 3); - assert!(moved_outside.graphemes.to_string().starts_with("second: 2")); - assert_eq!(moved_outside.cursor.unwrap().row, 2); - - state.document.up(); - let moved_back_inside = state.create_graphemes_in_viewport(80, 3); - assert!( - moved_back_inside - .graphemes - .to_string() - .starts_with("second: 2") - ); - assert_eq!(moved_back_inside.cursor.unwrap().row, 1); - - state.document.up(); - state.create_graphemes_in_viewport(80, 3); - state.document.up(); - let moved_above = state.create_graphemes_in_viewport(80, 3); - assert!(moved_above.graphemes.to_string().starts_with("first: 1")); - assert_eq!(moved_above.cursor.unwrap().row, 0); - } - - #[test] - fn configured_line_limit_bounds_viewport_projection() { - let value = serde_yaml::from_str("first: one\nsecond: two\n").unwrap(); - let state = State { - document: Document::new([&value]), - config: Config { - lines: Some(1), - ..Config::default() - }, - }; - - let projected = state.create_graphemes_in_viewport(80, 20); - assert_eq!(projected.graphemes.logical_lines().len(), 1); - } - - #[test] - fn preserves_expanded_line_numbers_after_toggle() { - let value = serde_yaml::from_str("first:\n nested: one\nlast: two\n").unwrap(); - let mut state = State { - document: Document::new([&value]), - config: Config { - show_line_numbers: true, - ..Default::default() - }, - }; - - assert_eq!(state.document.visible_line_numbers(), vec![1, 2, 3]); - - state.document.toggle(); - - assert_eq!(state.document.visible_line_numbers(), vec![1, 3]); - assert_eq!( - state.create_graphemes().graphemes.to_string(), - "1 first: {…}\n3 last: two" - ); - } - - #[test] - fn collapsed_root_containers_use_the_first_visible_line_number() { - for (source, expected) in [ - ("first: one\nsecond: two\n", "1 {…}"), - ("- first\n- second\n", "1 […]"), - ] { - let value = serde_yaml::from_str(source).unwrap(); - let mut state = State { - document: Document::new([&value]), - config: Config { - show_line_numbers: true, - ..Default::default() - }, - }; - - state.document.set_nodes_visibility(true); - - assert_eq!(state.create_graphemes().graphemes.to_string(), expected); + mod state { + use super::*; + + mod create_graphemes_in_viewport { + use super::*; + + #[test] + fn viewport_projection_is_bounded_and_resolves_hits() { + let value = serde_yaml::from_str( + "first: one\nsecond:\n nested: two\n extra: value\nthird: three\n", + ) + .unwrap(); + let mut state = State { + document: Document::new([&value]), + config: Config::default(), + }; + + state.create_graphemes_in_viewport(80, 2); + state.document.down(); + let projected = state.create_graphemes_in_viewport(80, 2); + let rendered = projected.graphemes.to_string(); + + assert!(rendered.contains("first: one")); + assert!(rendered.contains("second: ")); + assert!(!rendered.contains("nested: two")); + assert!(!rendered.contains("extra: value")); + assert!(!rendered.contains("third: three")); + assert_eq!(projected.cursor.unwrap().row, 1); + + let YamlHit::Toggle { row_index } = state + .hit_at_viewport(ContentPosition { row: 1, column: 4 }) + .unwrap(); + state.document.toggle_at(row_index); + + let collapsed = state.create_graphemes_in_viewport(80, 2); + assert!(collapsed.graphemes.to_string().contains("second: {…}")); + } + + #[test] + fn viewport_projection_stays_stable_until_cursor_leaves_it() { + let value = + serde_yaml::from_str("first: 1\nsecond: 2\nthird: 3\nfourth: 4\n").unwrap(); + let mut state = State { + document: Document::new([&value]), + config: Config::default(), + }; + + let initial = state.create_graphemes_in_viewport(80, 3); + assert!(initial.graphemes.to_string().starts_with("first: 1")); + assert_eq!(initial.cursor.unwrap().row, 0); + + state.document.down(); + let moved_inside = state.create_graphemes_in_viewport(80, 3); + assert!(moved_inside.graphemes.to_string().starts_with("first: 1")); + assert_eq!(moved_inside.cursor.unwrap().row, 1); + + state.document.down(); + let moved_to_edge = state.create_graphemes_in_viewport(80, 3); + assert!(moved_to_edge.graphemes.to_string().starts_with("first: 1")); + assert_eq!(moved_to_edge.cursor.unwrap().row, 2); + + state.document.down(); + let moved_outside = state.create_graphemes_in_viewport(80, 3); + assert!(moved_outside.graphemes.to_string().starts_with("second: 2")); + assert_eq!(moved_outside.cursor.unwrap().row, 2); + + state.document.up(); + let moved_back_inside = state.create_graphemes_in_viewport(80, 3); + assert!( + moved_back_inside + .graphemes + .to_string() + .starts_with("second: 2") + ); + assert_eq!(moved_back_inside.cursor.unwrap().row, 1); + + state.document.up(); + state.create_graphemes_in_viewport(80, 3); + state.document.up(); + let moved_above = state.create_graphemes_in_viewport(80, 3); + assert!(moved_above.graphemes.to_string().starts_with("first: 1")); + assert_eq!(moved_above.cursor.unwrap().row, 0); + } + + #[test] + fn configured_line_limit_bounds_viewport_projection() { + let value = serde_yaml::from_str("first: one\nsecond: two\n").unwrap(); + let state = State { + document: Document::new([&value]), + config: Config { + lines: Some(1), + ..Config::default() + }, + }; + + let projected = state.create_graphemes_in_viewport(80, 20); + assert_eq!(projected.graphemes.logical_lines().len(), 1); + } + + #[test] + fn viewport_projection_uses_stable_line_numbers() { + let value = + serde_yaml::from_str("first: one\nsecond: two\nthird: three\n").unwrap(); + let mut state = State { + document: Document::new([&value]), + config: Config { + show_line_numbers: true, + ..Default::default() + }, + }; + + state.create_graphemes_in_viewport(80, 2); + state.document.down(); + state.document.down(); + + assert_eq!( + state + .create_graphemes_in_viewport(80, 2) + .graphemes + .to_string(), + "2 second: two\n3 third: three" + ); + } } - } - #[test] - fn viewport_projection_uses_stable_line_numbers() { - let value = serde_yaml::from_str("first: one\nsecond: two\nthird: three\n").unwrap(); - let mut state = State { - document: Document::new([&value]), - config: Config { - show_line_numbers: true, - ..Default::default() - }, - }; - - state.create_graphemes_in_viewport(80, 2); - state.document.down(); - state.document.down(); - - assert_eq!( - state - .create_graphemes_in_viewport(80, 2) - .graphemes - .to_string(), - "2 second: two\n3 third: three" - ); + mod create_graphemes { + use super::*; + + #[test] + fn creates_full_content_and_resolves_visible_rows_for_hits() { + let value = + serde_yaml::from_str("first: one\nsecond:\n nested: two\nthird: three\n") + .unwrap(); + let mut state = State { + document: Document::new([&value]), + config: Config::default(), + }; + + let initial = state.create_graphemes(); + assert!(initial.graphemes.to_string().contains("first: one")); + assert!(initial.graphemes.to_string().contains("third: three")); + assert_eq!(initial.cursor.unwrap().row, 0); + + state.document.down(); + let moved = state.create_graphemes(); + assert!(moved.graphemes.to_string().contains("first: one")); + assert_eq!(moved.cursor.unwrap().row, 1); + + assert!(matches!( + state.hit_at(ContentPosition { row: 1, column: 4 }), + Some(YamlHit::Toggle { .. }) + )); + } + + #[test] + fn preserves_expanded_line_numbers_after_toggle() { + let value = serde_yaml::from_str("first:\n nested: one\nlast: two\n").unwrap(); + let mut state = State { + document: Document::new([&value]), + config: Config { + show_line_numbers: true, + ..Default::default() + }, + }; + + assert_eq!(state.document.visible_line_numbers(), vec![1, 2, 3]); + + state.document.toggle(); + + assert_eq!(state.document.visible_line_numbers(), vec![1, 3]); + assert_eq!( + state.create_graphemes().graphemes.to_string(), + "1 first: {…}\n3 last: two" + ); + } + + #[test] + fn collapsed_root_containers_use_the_first_visible_line_number() { + for (source, expected) in [ + ("first: one\nsecond: two\n", "1 {…}"), + ("- first\n- second\n", "1 […]"), + ] { + let value = serde_yaml::from_str(source).unwrap(); + let mut state = State { + document: Document::new([&value]), + config: Config { + show_line_numbers: true, + ..Default::default() + }, + }; + + state.document.set_nodes_visibility(true); + + assert_eq!(state.create_graphemes().graphemes.to_string(), expected); + } + } + } } } diff --git a/promkit-widgets/src/structured/yaml/config.rs b/promkit-widgets/src/structured/yaml/config.rs index cc3ad554..b4221f46 100644 --- a/promkit-widgets/src/structured/yaml/config.rs +++ b/promkit-widgets/src/structured/yaml/config.rs @@ -316,80 +316,84 @@ impl Config { mod tests { use super::*; - mod render_terminal_rows { + mod config { use super::*; - use crate::structured::ContainerNode; - - #[test] - fn renders_sequence_mapping_first_key_on_item_line() { - let rows = vec![ - Row { - depth: 1, - key: None, - is_sequence_item: true, - node: YamlNode::Container(ContainerNode::Open { - typ: ContainerType::Object, - collapsed: false, - close_index: 3, - }), - }, - Row { - depth: 2, - key: Some("name".to_string()), - is_sequence_item: false, - node: YamlNode::String("alice".to_string()), - }, - Row { - depth: 2, - key: Some("age".to_string()), - is_sequence_item: false, - node: YamlNode::Number(match serde_yaml::from_str("20").unwrap() { - serde_yaml::Value::Number(number) => number, - _ => unreachable!(), - }), - }, - ]; - - let lines = Config { - indent: 2, - overflow_mode: OverflowMode::Truncate, - ..Default::default() + + mod render_terminal_rows { + use super::*; + use crate::structured::ContainerNode; + + #[test] + fn renders_sequence_mapping_first_key_on_item_line() { + let rows = vec![ + Row { + depth: 1, + key: None, + is_sequence_item: true, + node: YamlNode::Container(ContainerNode::Open { + typ: ContainerType::Object, + collapsed: false, + close_index: 3, + }), + }, + Row { + depth: 2, + key: Some("name".to_string()), + is_sequence_item: false, + node: YamlNode::String("alice".to_string()), + }, + Row { + depth: 2, + key: Some("age".to_string()), + is_sequence_item: false, + node: YamlNode::Number(match serde_yaml::from_str("20").unwrap() { + serde_yaml::Value::Number(number) => number, + _ => unreachable!(), + }), + }, + ]; + + let lines = Config { + indent: 2, + overflow_mode: OverflowMode::Truncate, + ..Default::default() + } + .render_terminal_rows(&rows, 80) + .into_iter() + .map(|line| line.to_string()) + .collect::>(); + + assert_eq!( + lines, + vec!["- name: alice".to_string(), " age: 20".to_string(),] + ); } - .render_terminal_rows(&rows, 80) - .into_iter() - .map(|line| line.to_string()) - .collect::>(); - - assert_eq!( - lines, - vec!["- name: alice".to_string(), " age: 20".to_string(),] - ); - } - #[test] - fn does_not_indent_the_first_root_mapping_key() { - let value = serde_yaml::from_str("name: alice\naddress:\n city: Tokyo\n").unwrap(); - let document = crate::structured::yaml::Document::new([&value]); - let rows = document.extract_rows_from_current(usize::MAX); + #[test] + fn does_not_indent_the_first_root_mapping_key() { + let value = serde_yaml::from_str("name: alice\naddress:\n city: Tokyo\n").unwrap(); + let document = crate::structured::yaml::Document::new([&value]); + let rows = document.extract_rows_from_current(usize::MAX); - let lines = Config { - indent: 2, - overflow_mode: OverflowMode::Truncate, - ..Default::default() + let lines = Config { + indent: 2, + overflow_mode: OverflowMode::Truncate, + ..Default::default() + } + .render_terminal_rows(&rows, 80) + .into_iter() + .map(|line| line.to_string()) + .collect::>(); + + assert_eq!( + lines, + vec![ + "name: alice".to_string(), + "address: ".to_string(), + " city: Tokyo".to_string(), + ] + ); } - .render_terminal_rows(&rows, 80) - .into_iter() - .map(|line| line.to_string()) - .collect::>(); - - assert_eq!( - lines, - vec![ - "name: alice".to_string(), - "address: ".to_string(), - " city: Tokyo".to_string(), - ] - ); } } } diff --git a/promkit-widgets/src/structured/yaml/document.rs b/promkit-widgets/src/structured/yaml/document.rs index da7944df..7b0f27ae 100644 --- a/promkit-widgets/src/structured/yaml/document.rs +++ b/promkit-widgets/src/structured/yaml/document.rs @@ -280,48 +280,60 @@ second: [1, 2] Document::new(values.iter()) } - #[test] - fn from_str_matches_value_conversion() { - let expected = via_value(INPUT); - let actual = Document::from_str(INPUT).unwrap(); + mod from_str { + use super::*; - assert_eq!(actual.rows(), expected.rows()); - } + #[test] + fn matches_value_conversion() { + let expected = via_value(INPUT); + let actual = Document::from_str(INPUT).unwrap(); - #[test] - fn from_reader_matches_value_conversion() { - let expected = via_value(INPUT); - let actual = Document::from_reader(Cursor::new(INPUT.as_bytes())).unwrap(); + assert_eq!(actual.rows(), expected.rows()); + } - assert_eq!(actual.rows(), expected.rows()); - } + #[test] + fn matches_value_conversion_for_scalar_forms() { + for input in [ + "", + "null\n", + "~\n", + ".nan\n", + ".inf\n", + "-.inf\n", + "'quoted'\n", + "|\n multiline\n text\n", + "!!str 1\n", + "!!int '1'\n", + "---\n...\n---\n{}\n", + ] { + let expected = via_value(input); + let actual = Document::from_str(input).unwrap(); + + assert_eq!(actual.rows(), expected.rows(), "input: {input:?}"); + } + } - #[test] - fn direct_parsing_matches_value_conversion_for_scalar_forms() { - for input in [ - "", - "null\n", - "~\n", - ".nan\n", - ".inf\n", - "-.inf\n", - "'quoted'\n", - "|\n multiline\n text\n", - "!!str 1\n", - "!!int '1'\n", - "---\n...\n---\n{}\n", - ] { - let expected = via_value(input); - let actual = Document::from_str(input).unwrap(); - - assert_eq!(actual.rows(), expected.rows(), "input: {input:?}"); + #[test] + fn reports_invalid_yaml() { + assert!(Document::from_str("key: [unterminated").is_err()); + assert!(Document::from_str("duplicate: one\nduplicate: two\n").is_err()); } } - #[test] - fn direct_parsing_reports_invalid_yaml() { - assert!(Document::from_str("key: [unterminated").is_err()); - assert!(Document::from_reader(Cursor::new(b"key: {")).is_err()); - assert!(Document::from_str("duplicate: one\nduplicate: two\n").is_err()); + mod from_reader { + use super::*; + + #[test] + fn matches_value_conversion() { + let expected = via_value(INPUT); + let actual = Document::from_reader(Cursor::new(INPUT.as_bytes())).unwrap(); + + assert_eq!(actual.rows(), expected.rows()); + } + + #[test] + fn reports_invalid_yaml() { + assert!(Document::from_reader(Cursor::new(b"key: {")).is_err()); + } } } diff --git a/promkit-widgets/src/table.rs b/promkit-widgets/src/table.rs index 3a6230de..f49588fd 100644 --- a/promkit-widgets/src/table.rs +++ b/promkit-widgets/src/table.rs @@ -308,116 +308,137 @@ mod tests { State::new(Document::from_csv(input.as_bytes(), CsvOptions::default()).unwrap()) } - #[test] - fn parses_quoted_and_multiline_cells_without_per_cell_ownership() { - let document = Document::from_csv( - "name,note\nalice,\"hello, world\"\nbob,\"line 1\nline 2\"\n".as_bytes(), - CsvOptions::default(), - ) - .unwrap(); - - assert_eq!(document.row_count(), 2); - assert_eq!(document.column_count(), 2); - assert_eq!(document.header_cell(1), Some("note")); - assert_eq!(document.cell(0, 1), Some("hello, world")); - assert_eq!(document.cell(1, 1), Some("line 1\nline 2")); - } + mod document { + use super::*; + + mod from_csv { + use super::*; + + #[test] + fn parses_quoted_and_multiline_cells_without_per_cell_ownership() { + let document = Document::from_csv( + "name,note\nalice,\"hello, world\"\nbob,\"line 1\nline 2\"\n".as_bytes(), + CsvOptions::default(), + ) + .unwrap(); + + assert_eq!(document.row_count(), 2); + assert_eq!(document.column_count(), 2); + assert_eq!(document.header_cell(1), Some("note")); + assert_eq!(document.cell(0, 1), Some("hello, world")); + assert_eq!(document.cell(1, 1), Some("line 1\nline 2")); + } - #[test] - fn supports_a_non_comma_delimiter() { - let document = Document::from_csv( - "name\tvalue\nfirst\tone\n".as_bytes(), - CsvOptions::default().delimiter(b'\t'), - ) - .unwrap(); + #[test] + fn supports_a_non_comma_delimiter() { + let document = Document::from_csv( + "name\tvalue\nfirst\tone\n".as_bytes(), + CsvOptions::default().delimiter(b'\t'), + ) + .unwrap(); - assert_eq!(document.cell(0, 1), Some("one")); - } + assert_eq!(document.cell(0, 1), Some("one")); + } - #[test] - fn vertical_projection_is_bounded_and_follows_the_cursor() { - let mut state = state("id,value\n1,one\n2,two\n3,three\n4,four\n"); - - let initial = state.create_graphemes_in_viewport(40, 3); - assert_eq!(initial.graphemes.logical_lines().len(), 3); - assert!(initial.graphemes.to_string().contains("1")); - assert!(initial.graphemes.to_string().contains("2")); - - state.document.down(); - state.document.down(); - let moved = state.create_graphemes_in_viewport(40, 3); - assert!(!moved.graphemes.to_string().contains("1 ")); - assert!(moved.graphemes.to_string().contains("3")); - assert_eq!(moved.cursor.unwrap().row, 2); + #[test] + fn rejects_non_rectangular_input() { + assert!( + Document::from_csv("a,b\none,two\nthree\n".as_bytes(), CsvOptions::default()) + .is_err() + ); + } + } } - #[test] - fn horizontal_projection_scrolls_by_display_cell() { - let mut state = state("abc,def\none,two\n"); - state.config.separator = "|".to_owned(); + mod state { + use super::*; - let first = state.create_graphemes_in_viewport(2, 2); - assert!(first.graphemes.to_string().starts_with("ab")); + mod create_graphemes_in_viewport { + use super::*; - state.document.scroll_right_by(1); - let second = state.create_graphemes_in_viewport(2, 2); - assert!(second.graphemes.to_string().starts_with("bc")); - assert_eq!(state.document.horizontal_offset(), 1); - } + #[test] + fn vertical_projection_is_bounded_and_follows_the_cursor() { + let mut state = state("id,value\n1,one\n2,two\n3,three\n4,four\n"); - #[test] - fn horizontal_projection_preserves_cell_suffixes_without_ellipsis() { - let mut state = state("value\nabcdefghijklmnopqrstuvwxyz\n"); - let initial = state.create_graphemes_in_viewport(10, 2); - assert!(!initial.graphemes.to_string().contains('…')); - - assert!(state.document.scroll_right_by(16)); - let scrolled = state.create_graphemes_in_viewport(10, 2); - let lines = scrolled.graphemes.logical_lines(); - assert_eq!(lines[1].to_string(), "qrstuvwxyz"); - assert!(!scrolled.graphemes.to_string().contains('…')); - } + let initial = state.create_graphemes_in_viewport(40, 3); + assert_eq!(initial.graphemes.logical_lines().len(), 3); + assert!(initial.graphemes.to_string().contains("1")); + assert!(initial.graphemes.to_string().contains("2")); - #[test] - fn projection_replaces_embedded_newlines_and_respects_width() { - let mut state = state("name,note\nalice,\"line 1\nline 2\"\n"); - state.create_graphemes_in_viewport(10, 2); - state.document.scroll_to_end(); - let projected = state.create_graphemes_in_viewport(10, 2); - - assert_eq!(projected.graphemes.logical_lines().len(), 2); - assert!(projected.graphemes.to_string().contains('↵')); - assert!( - projected - .graphemes - .logical_lines() - .iter() - .all(|line| line.widths() <= 10) - ); - } + state.document.down(); + state.document.down(); + let moved = state.create_graphemes_in_viewport(40, 3); + assert!(!moved.graphemes.to_string().contains("1 ")); + assert!(moved.graphemes.to_string().contains("3")); + assert_eq!(moved.cursor.unwrap().row, 2); + } - #[test] - fn resolves_header_and_body_hits_in_the_latest_viewport() { - let mut state = state("a,b,c\nx,y,z\n"); - state.config.separator = "|".to_owned(); - state.create_graphemes_in_viewport(3, 2); - state.document.scroll_right_by(2); - state.create_graphemes_in_viewport(3, 2); - - assert_eq!( - state.hit_at_viewport(ContentPosition { row: 0, column: 0 }), - Some(TableHit::Header { column: 1 }) - ); - assert_eq!( - state.hit_at_viewport(ContentPosition { row: 1, column: 0 }), - Some(TableHit::Cell { row: 0, column: 1 }) - ); - } + #[test] + fn horizontal_projection_scrolls_by_display_cell() { + let mut state = state("abc,def\none,two\n"); + state.config.separator = "|".to_owned(); + + let first = state.create_graphemes_in_viewport(2, 2); + assert!(first.graphemes.to_string().starts_with("ab")); - #[test] - fn rejects_non_rectangular_csv() { - assert!( - Document::from_csv("a,b\none,two\nthree\n".as_bytes(), CsvOptions::default()).is_err() - ); + state.document.scroll_right_by(1); + let second = state.create_graphemes_in_viewport(2, 2); + assert!(second.graphemes.to_string().starts_with("bc")); + assert_eq!(state.document.horizontal_offset(), 1); + } + + #[test] + fn horizontal_projection_preserves_cell_suffixes_without_ellipsis() { + let mut state = state("value\nabcdefghijklmnopqrstuvwxyz\n"); + let initial = state.create_graphemes_in_viewport(10, 2); + assert!(!initial.graphemes.to_string().contains('…')); + + assert!(state.document.scroll_right_by(16)); + let scrolled = state.create_graphemes_in_viewport(10, 2); + let lines = scrolled.graphemes.logical_lines(); + assert_eq!(lines[1].to_string(), "qrstuvwxyz"); + assert!(!scrolled.graphemes.to_string().contains('…')); + } + + #[test] + fn projection_replaces_embedded_newlines_and_respects_width() { + let mut state = state("name,note\nalice,\"line 1\nline 2\"\n"); + state.create_graphemes_in_viewport(10, 2); + state.document.scroll_to_end(); + let projected = state.create_graphemes_in_viewport(10, 2); + + assert_eq!(projected.graphemes.logical_lines().len(), 2); + assert!(projected.graphemes.to_string().contains('↵')); + assert!( + projected + .graphemes + .logical_lines() + .iter() + .all(|line| line.widths() <= 10) + ); + } + } + + mod hit_at_viewport { + use super::*; + + #[test] + fn resolves_header_and_body_in_the_latest_viewport() { + let mut state = state("a,b,c\nx,y,z\n"); + state.config.separator = "|".to_owned(); + state.create_graphemes_in_viewport(3, 2); + state.document.scroll_right_by(2); + state.create_graphemes_in_viewport(3, 2); + + assert_eq!( + state.hit_at_viewport(ContentPosition { row: 0, column: 0 }), + Some(TableHit::Header { column: 1 }) + ); + assert_eq!( + state.hit_at_viewport(ContentPosition { row: 1, column: 0 }), + Some(TableHit::Cell { row: 0, column: 1 }) + ); + } + } } } diff --git a/promkit-widgets/src/text.rs b/promkit-widgets/src/text.rs index c3df5af1..aeac507f 100644 --- a/promkit-widgets/src/text.rs +++ b/promkit-widgets/src/text.rs @@ -79,20 +79,28 @@ pub enum TextHit { } #[cfg(test)] -mod state_tests { +mod tests { use super::*; - #[test] - fn resolves_line_rows_for_hits() { - let state = State { - text: Text::from("first\nsecond"), - config: Config::default(), - }; + mod state { + use super::*; - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 20 }), - Some(TextHit::Select { index: 1 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + mod hit_at { + use super::*; + + #[test] + fn resolves_line_rows() { + let state = State { + text: Text::from("first\nsecond"), + config: Config::default(), + }; + + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 20 }), + Some(TextHit::Select { index: 1 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + } + } } } diff --git a/promkit-widgets/src/text/config.rs b/promkit-widgets/src/text/config.rs index ecda1693..b1c857b7 100644 --- a/promkit-widgets/src/text/config.rs +++ b/promkit-widgets/src/text/config.rs @@ -15,13 +15,13 @@ pub struct Config { #[cfg(test)] mod tests { #[cfg(feature = "serde")] - mod serde_compatibility { + mod deserialize { use promkit_core::crossterm::style::{Attribute, Color}; use super::super::Config; #[test] - fn config_fields_are_fully_loaded_from_toml() { + fn loads_all_fields_from_toml() { let input = r#" style = "fg=yellow,attr=bold" lines = 2 diff --git a/promkit-widgets/src/text/text.rs b/promkit-widgets/src/text/text.rs index 7bd0e5cd..62ef9b59 100644 --- a/promkit-widgets/src/text/text.rs +++ b/promkit-widgets/src/text/text.rs @@ -104,40 +104,70 @@ mod tests { use super::Text; - #[test] - fn empty_input_creates_no_lines() { - let text = Text::from(""); - assert!(text.items().is_empty()); + mod from { + use super::*; + + #[test] + fn empty_input_creates_no_lines() { + let text = Text::from(""); + assert!(text.items().is_empty()); + } + + #[test] + fn explicit_empty_lines_are_preserved() { + let text = Text::from("a\n\nb"); + assert_eq!(text.items().len(), 3); + assert_eq!(text.items()[1].chars(), vec!['\0']); + } } - #[test] - fn explicit_empty_lines_are_preserved() { - let text = Text::from("a\n\nb"); - assert_eq!(text.items().len(), 3); - assert_eq!(text.items()[1].chars(), vec!['\0']); + mod replace_contents { + use super::*; + + #[test] + fn restores_a_current_line_for_empty_text() { + let mut text = Text::default(); + text.replace_contents(vec![StyledGraphemes::from("first")]); + + assert_eq!(text.current_line(), Some(0)); + } } - #[test] - fn replacing_empty_text_restores_a_current_line() { - let mut text = Text::default(); - text.replace_contents(vec![StyledGraphemes::from("first")]); + mod backward { + use super::*; + + #[test] + fn stops_at_the_first_line() { + let mut text = Text::from("first\nsecond"); - assert_eq!(text.current_line(), Some(0)); + assert!(!text.backward()); + assert_eq!(text.current_line(), Some(0)); + } } - #[test] - fn navigation_stops_at_the_text_boundaries() { - let mut text = Text::from("first\nsecond"); + mod forward { + use super::*; + + #[test] + fn stops_at_the_last_line() { + let mut text = Text::from("first\nsecond"); - assert_eq!(text.current_line(), Some(0)); - assert!(!text.backward()); - assert!(text.forward()); - assert_eq!(text.current_line(), Some(1)); - assert!(!text.forward()); + assert!(text.forward()); + assert_eq!(text.current_line(), Some(1)); + assert!(!text.forward()); + assert_eq!(text.current_line(), Some(1)); + } + } - assert!(text.move_to(0)); - assert_eq!(text.current_line(), Some(0)); - assert!(!text.move_to(2)); - assert_eq!(text.current_line(), Some(0)); + mod move_to { + use super::*; + + #[test] + fn rejects_an_out_of_bounds_line() { + let mut text = Text::from("first\nsecond"); + + assert!(!text.move_to(2)); + assert_eq!(text.current_line(), Some(0)); + } } } diff --git a/promkit-widgets/src/text_editor.rs b/promkit-widgets/src/text_editor.rs index fd9eb2d0..9a156e26 100644 --- a/promkit-widgets/src/text_editor.rs +++ b/promkit-widgets/src/text_editor.rs @@ -144,7 +144,7 @@ pub enum TextEditorHit { } #[cfg(test)] -mod state_tests { +mod tests { use super::*; fn state(text: &str, prefix: &str) -> State { @@ -158,158 +158,170 @@ mod state_tests { } } - #[test] - fn resolves_prefix_input_and_trailing_columns() { - let state = state("abc", ">> "); - - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 1 }), - Some(TextEditorHit::Cursor { index: 0 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 4 }), - Some(TextEditorHit::Cursor { index: 1 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 80 }), - Some(TextEditorHit::Cursor { index: 3 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 1, column: 0 }), None); - } + mod state { + use super::*; + + mod hit_at { + use super::*; + + #[test] + fn resolves_prefix_input_and_trailing_columns() { + let state = state("abc", ">> "); + + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 1 }), + Some(TextEditorHit::Cursor { index: 0 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 4 }), + Some(TextEditorHit::Cursor { index: 1 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 80 }), + Some(TextEditorHit::Cursor { index: 3 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 1, column: 0 }), None); + } - #[test] - fn resolves_columns_inside_wide_characters() { - let state = state("界a", ""); - - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 0 }), - Some(TextEditorHit::Cursor { index: 0 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 1 }), - Some(TextEditorHit::Cursor { index: 0 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 2 }), - Some(TextEditorHit::Cursor { index: 1 }) - ); - } + #[test] + fn resolves_columns_inside_wide_characters() { + let state = state("界a", ""); + + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 0 }), + Some(TextEditorHit::Cursor { index: 0 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 1 }), + Some(TextEditorHit::Cursor { index: 0 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 2 }), + Some(TextEditorHit::Cursor { index: 1 }) + ); + } - #[test] - fn uses_the_rendered_mask_width() { - let mut state = state("界a", ""); - state.config.mask = Some('*'); + #[test] + fn uses_the_rendered_mask_width() { + let mut state = state("界a", ""); + state.config.mask = Some('*'); - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 1 }), - Some(TextEditorHit::Cursor { index: 1 }) - ); - } + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 1 }), + Some(TextEditorHit::Cursor { index: 1 }) + ); + } - #[test] - fn renders_a_multiline_cursor_at_its_logical_position() { - let state = state("ab\n界c", ">> "); + #[test] + fn resolves_clicks_on_each_multiline_row() { + let state = state("ab\n界c", ">> "); + + assert_eq!( + state.hit_at(ContentPosition { row: 0, column: 80 }), + Some(TextEditorHit::Cursor { index: 2 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 0 }), + Some(TextEditorHit::Cursor { index: 3 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 1 }), + Some(TextEditorHit::Cursor { index: 3 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 2 }), + Some(TextEditorHit::Cursor { index: 4 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 80 }), + Some(TextEditorHit::Cursor { index: 5 }) + ); + assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); + } - let created = state.create_graphemes(); + #[test] + fn resolves_clicks_on_empty_multiline_rows() { + let state = state("a\n\nb", ""); + + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 0 }), + Some(TextEditorHit::Cursor { index: 2 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 2, column: 0 }), + Some(TextEditorHit::Cursor { index: 3 }) + ); + } - assert_eq!(">> ab\n界c ", created.graphemes.to_string()); - assert_eq!(Some(ContentPosition { row: 1, column: 3 }), created.cursor); - } + #[test] + fn uses_the_continuation_prefix_width_for_multiline_hits() { + let mut state = state("first\nsecond", ">>> "); + state.config.continuation_prefix = "... ".into(); + + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 2 }), + Some(TextEditorHit::Cursor { index: 6 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 5 }), + Some(TextEditorHit::Cursor { index: 7 }) + ); + assert_eq!( + state.hit_at(ContentPosition { row: 1, column: 80 }), + Some(TextEditorHit::Cursor { index: 12 }) + ); + } + } - #[test] - fn renders_a_visible_cursor_before_a_newline() { - let mut state = state("ab\ncd", ""); - assert!(state.texteditor.move_to(2)); + mod create_graphemes { + use super::*; - let created = state.create_graphemes(); + #[test] + fn renders_a_multiline_cursor_at_its_logical_position() { + let state = state("ab\n界c", ">> "); - assert_eq!("ab \ncd ", created.graphemes.to_string()); - assert_eq!(Some(ContentPosition { row: 0, column: 2 }), created.cursor); - } + let created = state.create_graphemes(); - #[test] - fn resolves_clicks_on_each_multiline_row() { - let state = state("ab\n界c", ">> "); - - assert_eq!( - state.hit_at(ContentPosition { row: 0, column: 80 }), - Some(TextEditorHit::Cursor { index: 2 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 0 }), - Some(TextEditorHit::Cursor { index: 3 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 1 }), - Some(TextEditorHit::Cursor { index: 3 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 2 }), - Some(TextEditorHit::Cursor { index: 4 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 80 }), - Some(TextEditorHit::Cursor { index: 5 }) - ); - assert_eq!(state.hit_at(ContentPosition { row: 2, column: 0 }), None); - } + assert_eq!(">> ab\n界c ", created.graphemes.to_string()); + assert_eq!(Some(ContentPosition { row: 1, column: 3 }), created.cursor); + } - #[test] - fn resolves_clicks_on_empty_multiline_rows() { - let state = state("a\n\nb", ""); - - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 0 }), - Some(TextEditorHit::Cursor { index: 2 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 2, column: 0 }), - Some(TextEditorHit::Cursor { index: 3 }) - ); - } + #[test] + fn renders_a_visible_cursor_before_a_newline() { + let mut state = state("ab\ncd", ""); + assert!(state.texteditor.move_to(2)); - #[test] - fn masking_preserves_multiline_layout_and_cursor_position() { - let mut state = state("ab\n界c", ">> "); - state.config.mask = Some('*'); + let created = state.create_graphemes(); - let created = state.create_graphemes(); + assert_eq!("ab \ncd ", created.graphemes.to_string()); + assert_eq!(Some(ContentPosition { row: 0, column: 2 }), created.cursor); + } - assert_eq!(">> **\n** ", created.graphemes.to_string()); - assert_eq!(Some(ContentPosition { row: 1, column: 2 }), created.cursor); - } + #[test] + fn masking_preserves_multiline_layout_and_cursor_position() { + let mut state = state("ab\n界c", ">> "); + state.config.mask = Some('*'); - #[test] - fn renders_a_continuation_prefix_without_changing_the_editor_text() { - let mut state = state("first\nsecond", ">>> "); - state.config.continuation_prefix = "... ".into(); + let created = state.create_graphemes(); - let created = state.create_graphemes(); + assert_eq!(">> **\n** ", created.graphemes.to_string()); + assert_eq!(Some(ContentPosition { row: 1, column: 2 }), created.cursor); + } - assert_eq!( - state.texteditor.text_without_cursor().to_string(), - "first\nsecond" - ); - assert_eq!(created.graphemes.to_string(), ">>> first\n... second "); - assert_eq!(created.cursor, Some(ContentPosition { row: 1, column: 10 })); - } + #[test] + fn renders_a_continuation_prefix_without_changing_the_editor_text() { + let mut state = state("first\nsecond", ">>> "); + state.config.continuation_prefix = "... ".into(); + + let created = state.create_graphemes(); - #[test] - fn uses_the_continuation_prefix_width_for_multiline_hits() { - let mut state = state("first\nsecond", ">>> "); - state.config.continuation_prefix = "... ".into(); - - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 2 }), - Some(TextEditorHit::Cursor { index: 6 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 5 }), - Some(TextEditorHit::Cursor { index: 7 }) - ); - assert_eq!( - state.hit_at(ContentPosition { row: 1, column: 80 }), - Some(TextEditorHit::Cursor { index: 12 }) - ); + assert_eq!( + state.texteditor.text_without_cursor().to_string(), + "first\nsecond" + ); + assert_eq!(created.graphemes.to_string(), ">>> first\n... second "); + assert_eq!(created.cursor, Some(ContentPosition { row: 1, column: 10 })); + } + } } } diff --git a/promkit-widgets/src/text_editor/config.rs b/promkit-widgets/src/text_editor/config.rs index aec13be7..6ced91e7 100644 --- a/promkit-widgets/src/text_editor/config.rs +++ b/promkit-widgets/src/text_editor/config.rs @@ -38,7 +38,7 @@ pub struct Config { #[cfg(test)] mod tests { #[cfg(feature = "serde")] - mod serde_compatibility { + mod deserialize { use std::collections::HashSet; use promkit_core::crossterm::style::{Attribute, Color}; @@ -46,7 +46,7 @@ mod tests { use super::super::{Config, Mode}; #[test] - fn config_fields_are_fully_loaded_from_toml() { + fn loads_all_fields_from_toml() { let input = r#" prefix = ">> " continuation_prefix = "... " diff --git a/promkit-widgets/src/text_editor/history.rs b/promkit-widgets/src/text_editor/history.rs index bb931def..f00ac3d3 100644 --- a/promkit-widgets/src/text_editor/history.rs +++ b/promkit-widgets/src/text_editor/history.rs @@ -191,21 +191,16 @@ impl History { } #[cfg(test)] -mod test { - mod position { +mod tests { + mod backward { use super::super::*; #[test] - fn navigation_stops_at_the_history_boundaries() { + fn stops_at_the_oldest_entry() { let mut history = History::default(); history.insert("first"); history.insert("second"); - assert_eq!(history.position, 2); - assert_eq!(history.get(), ""); - assert!(!history.forward()); - assert_eq!(history.position, 2); - assert!(history.backward()); assert_eq!(history.position, 1); assert_eq!(history.get(), "second"); @@ -214,11 +209,36 @@ mod test { assert_eq!(history.get(), "first"); assert!(!history.backward()); assert_eq!(history.position, 0); + } + } + + mod forward { + use super::super::*; + #[test] + fn stops_at_the_editing_slot() { + let mut history = History::default(); + history.insert("first"); + + assert!(!history.forward()); + assert_eq!(history.position, 1); + assert!(history.backward()); assert!(history.forward()); assert_eq!(history.position, 1); + } + } + + mod move_to_tail { + use super::super::*; + + #[test] + fn selects_the_editing_slot() { + let mut history = History::default(); + history.insert("first"); + history.backward(); + history.move_to_tail(); - assert_eq!(history.position, 2); + assert_eq!(history.position, 1); } } @@ -226,7 +246,7 @@ mod test { use super::super::*; #[test] - fn test() { + fn appends_an_entry_before_the_editing_slot() { let mut h = History::default(); h.insert("item"); assert_eq!( @@ -236,7 +256,7 @@ mod test { } #[test] - fn test_with_multiple_items() { + fn preserves_insertion_order() { let mut h = History::default(); h.insert("item1"); h.insert("item2"); @@ -247,7 +267,7 @@ mod test { } #[test] - fn test_with_limit_size() { + fn evicts_the_oldest_entry_at_the_size_limit() { let mut h = History { limit_size: Some(2), ..Default::default() @@ -266,7 +286,7 @@ mod test { use super::super::*; #[test] - fn test() { + fn reports_whether_an_entry_exists() { let mut h = History::default(); h.insert("existed"); assert!(h.exists("existed")); diff --git a/promkit-widgets/src/text_editor/text_editor.rs b/promkit-widgets/src/text_editor/text_editor.rs index 1d2d8d36..2a6ba8ef 100644 --- a/promkit-widgets/src/text_editor/text_editor.rs +++ b/promkit-widgets/src/text_editor/text_editor.rs @@ -394,593 +394,646 @@ impl TextEditor { } #[cfg(test)] -mod test { +mod tests { use super::*; - fn new_with_position(s: String, p: usize) -> TextEditor { - let text = StyledGraphemes::from(s); - TextEditor { - position: p.min(text.len().saturating_sub(1)), - text, - preferred_column: None, + mod text_editor { + use super::*; + + fn new_with_position(s: String, p: usize) -> TextEditor { + let text = StyledGraphemes::from(s); + TextEditor { + position: p.min(text.len().saturating_sub(1)), + text, + preferred_column: None, + } } - } - mod position { - use super::*; + mod new { + use super::*; - #[test] - fn starts_at_the_trailing_cursor() { - let texteditor = TextEditor::new("abc"); + #[test] + fn starts_at_the_trailing_cursor() { + let texteditor = TextEditor::new("abc"); - assert_eq!(texteditor.position(), 3); + assert_eq!(texteditor.position(), 3); + } } - #[test] - fn direct_and_relative_moves_preserve_position_on_failure() { - let mut texteditor = TextEditor::new("abc"); + mod move_to { + use super::*; - assert!(texteditor.move_to(1)); - assert_eq!(texteditor.position(), 1); - assert!(!texteditor.move_to(4)); - assert_eq!(texteditor.position(), 1); + #[test] + fn preserves_the_position_when_the_target_is_out_of_bounds() { + let mut texteditor = TextEditor::new("abc"); - assert!(texteditor.shift(0, 2)); - assert_eq!(texteditor.position(), 3); - assert!(!texteditor.shift(0, 1)); - assert_eq!(texteditor.position(), 3); - assert!(!texteditor.shift(4, 0)); - assert_eq!(texteditor.position(), 3); + assert!(texteditor.move_to(1)); + assert_eq!(texteditor.position(), 1); + assert!(!texteditor.move_to(4)); + assert_eq!(texteditor.position(), 1); + } } - } - mod masking { - use super::*; + mod shift { + use super::*; - #[test] - fn test() { - let txt = new_with_position(String::from("abcde "), 0); - assert_eq!(StyledGraphemes::from("***** "), txt.masking('*')) + #[test] + fn preserves_the_position_when_the_target_is_out_of_bounds() { + let mut texteditor = TextEditor::new("abc"); + assert!(texteditor.move_to(1)); + + assert!(texteditor.shift(0, 2)); + assert_eq!(texteditor.position(), 3); + assert!(!texteditor.shift(0, 1)); + assert_eq!(texteditor.position(), 3); + assert!(!texteditor.shift(4, 0)); + assert_eq!(texteditor.position(), 3); + } } - #[test] - fn preserves_newlines_in_multiline_text() { - let txt = TextEditor::new("ab\nc"); + mod masking { + use super::*; + + #[test] + fn replaces_non_whitespace_characters_with_the_mask() { + let txt = new_with_position(String::from("abcde "), 0); + assert_eq!(StyledGraphemes::from("***** "), txt.masking('*')) + } + + #[test] + fn preserves_newlines_in_multiline_text() { + let txt = TextEditor::new("ab\nc"); - assert_eq!(StyledGraphemes::from("**\n* "), txt.masking('*')); + assert_eq!(StyledGraphemes::from("**\n* "), txt.masking('*')); + } } - } - mod erase { - use super::*; + mod erase { + use super::*; - #[test] - fn test_for_empty() { - let txt = TextEditor::default(); - assert_eq!(StyledGraphemes::from(" "), txt.text()); - assert_eq!(0, txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("bc "), - 0, // indicate `b`. - ); - txt.erase(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - let new = new_with_position( - String::from("ab "), - 2, // indicate tail. - ); - txt.erase(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_head() { - let txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - assert_eq!(StyledGraphemes::from("abc "), txt.text()); - assert_eq!(0, txt.position()); - } - } - - mod find_previous_nearest_index { - use super::*; + #[test] + fn empty_editor_is_unchanged() { + let txt = TextEditor::default(); + assert_eq!(StyledGraphemes::from(" "), txt.text()); + assert_eq!(0, txt.position()); + } - use std::collections::HashSet; + #[test] + fn removes_the_character_before_the_cursor() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("bc "), + 0, // indicate `b`. + ); + txt.erase(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn removes_the_last_character_at_the_tail() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + let new = new_with_position( + String::from("ab "), + 2, // indicate tail. + ); + txt.erase(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } - #[test] - fn test() { - let mut txt = new_with_position(String::from("koko momo jojo "), 11); // indicate `o`. - assert_eq!(10, txt.find_previous_nearest_index(&HashSet::from([' ']))); - txt.move_to(10); - assert_eq!(5, txt.find_previous_nearest_index(&HashSet::from([' ']))); + #[test] + fn head_is_unchanged() { + let txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + assert_eq!(StyledGraphemes::from("abc "), txt.text()); + assert_eq!(0, txt.position()); + } } - #[test] - fn test_with_no_target() { - let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`. - assert_eq!(0, txt.find_previous_nearest_index(&HashSet::from(['z']))); + mod find_previous_nearest_index { + use super::*; + + use std::collections::HashSet; + + #[test] + fn finds_the_previous_word_boundary() { + let mut txt = new_with_position(String::from("koko momo jojo "), 11); // indicate `o`. + assert_eq!(10, txt.find_previous_nearest_index(&HashSet::from([' ']))); + txt.move_to(10); + assert_eq!(5, txt.find_previous_nearest_index(&HashSet::from([' ']))); + } + + #[test] + fn returns_the_head_when_no_boundary_exists() { + let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`. + assert_eq!(0, txt.find_previous_nearest_index(&HashSet::from(['z']))); + } } - } - mod find_next_nearest_index { - use super::*; + mod find_next_nearest_index { + use super::*; - use std::collections::HashSet; + use std::collections::HashSet; - #[test] - fn test() { - let mut txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`. - assert_eq!(10, txt.find_next_nearest_index(&HashSet::from([' ']))); - txt.move_to(10); - assert_eq!(14, txt.find_next_nearest_index(&HashSet::from([' ']))); + #[test] + fn finds_the_next_word_boundary() { + let mut txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`. + assert_eq!(10, txt.find_next_nearest_index(&HashSet::from([' ']))); + txt.move_to(10); + assert_eq!(14, txt.find_next_nearest_index(&HashSet::from([' ']))); + } + + #[test] + fn returns_the_tail_when_no_boundary_exists() { + let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`. + assert_eq!(14, txt.find_next_nearest_index(&HashSet::from(['z']))); + } } - #[test] - fn test_with_no_target() { - let txt = new_with_position(String::from("koko momo jojo "), 7); // indicate `m`. - assert_eq!(14, txt.find_next_nearest_index(&HashSet::from(['z']))); + mod insert { + use super::*; + + #[test] + fn inserts_into_an_empty_editor() { + let mut txt = TextEditor::default(); + let new = new_with_position( + String::from("d "), + 1, // indicate tail. + ); + txt.insert('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn inserts_before_the_cursor() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("adbc "), + 2, // indicate `b`. + ); + txt.insert('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn appends_at_the_tail() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + let new = new_with_position( + String::from("abcd "), + 4, // indicate tail. + ); + txt.insert('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn prepends_at_the_head() { + let mut txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + let new = new_with_position( + String::from("dabc "), + 1, // indicate `a`. + ); + txt.insert('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } } - } - mod insert { - use super::*; + mod logical_position { + use super::*; - #[test] - fn test_for_empty() { - let mut txt = TextEditor::default(); - let new = new_with_position( - String::from("d "), - 1, // indicate tail. - ); - txt.insert('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("adbc "), - 2, // indicate `b`. - ); - txt.insert('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - let new = new_with_position( - String::from("abcd "), - 4, // indicate tail. - ); - txt.insert('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_head() { - let mut txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - let new = new_with_position( - String::from("dabc "), - 1, // indicate `a`. - ); - txt.insert('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - } - - mod multiline { - use super::*; + #[test] + fn uses_display_columns() { + let mut txt = TextEditor::new("ab\n界c"); - #[test] - fn reports_logical_position_using_display_columns() { - let mut txt = TextEditor::new("ab\n界c"); + assert_eq!(TextPosition { row: 1, column: 3 }, txt.logical_position()); - assert_eq!(TextPosition { row: 1, column: 3 }, txt.logical_position()); + assert!(txt.move_to(3)); // Before `界`. + assert_eq!(TextPosition { row: 1, column: 0 }, txt.logical_position()); + } - assert!(txt.move_to(3)); // Before `界`. - assert_eq!(TextPosition { row: 1, column: 0 }, txt.logical_position()); + #[test] + fn handles_empty_lines_and_a_trailing_newline() { + let mut txt = TextEditor::new("a\n\n"); + + assert_eq!(TextPosition { row: 2, column: 0 }, txt.logical_position()); + assert!(txt.move_up()); + assert_eq!(TextPosition { row: 1, column: 0 }, txt.logical_position()); + assert!(txt.move_up()); + assert_eq!(TextPosition { row: 0, column: 0 }, txt.logical_position()); + } } - #[test] - fn inserts_a_newline_at_the_cursor() { - let mut txt = TextEditor::new("abcd"); - assert!(txt.move_to(2)); + mod insert_newline { + use super::*; + + #[test] + fn inserts_at_the_cursor() { + let mut txt = TextEditor::new("abcd"); + assert!(txt.move_to(2)); - txt.insert_newline(); + txt.insert_newline(); - assert_eq!("ab\ncd", txt.text_without_cursor().to_string()); - assert_eq!(3, txt.position()); - assert_eq!(TextPosition { row: 1, column: 0 }, txt.logical_position()); + assert_eq!("ab\ncd", txt.text_without_cursor().to_string()); + assert_eq!(3, txt.position()); + assert_eq!(TextPosition { row: 1, column: 0 }, txt.logical_position()); + } } - #[test] - fn moves_vertically_and_preserves_the_preferred_display_column() { - let mut txt = TextEditor::new("abcdef\nxy\n123456"); - assert!(txt.move_to(6)); // End of the first line. + mod move_down { + use super::*; + + #[test] + fn preserves_the_preferred_display_column() { + let mut txt = TextEditor::new("abcdef\nxy\n123456"); + assert!(txt.move_to(6)); // End of the first line. + + assert!(txt.move_down()); + assert_eq!(9, txt.position()); // End of the shorter second line. + assert_eq!(TextPosition { row: 1, column: 2 }, txt.logical_position()); + + assert!(txt.move_down()); + assert_eq!(16, txt.position()); // Restore column 6 on the third line. + assert_eq!(TextPosition { row: 2, column: 6 }, txt.logical_position()); + + assert!(txt.move_up()); + assert_eq!(9, txt.position()); + assert!(txt.move_up()); + assert_eq!(6, txt.position()); + } - assert!(txt.move_down()); - assert_eq!(9, txt.position()); // End of the shorter second line. - assert_eq!(TextPosition { row: 1, column: 2 }, txt.logical_position()); + #[test] + fn uses_wide_character_display_widths() { + let mut txt = TextEditor::new("界a\n123"); + assert!(txt.move_to(1)); // Display column 2 after `界`. - assert!(txt.move_down()); - assert_eq!(16, txt.position()); // Restore column 6 on the third line. - assert_eq!(TextPosition { row: 2, column: 6 }, txt.logical_position()); + assert!(txt.move_down()); - assert!(txt.move_up()); - assert_eq!(9, txt.position()); - assert!(txt.move_up()); - assert_eq!(6, txt.position()); + assert_eq!(5, txt.position()); + assert_eq!(TextPosition { row: 1, column: 2 }, txt.logical_position()); + } + + #[test] + fn stops_at_the_document_tail() { + let mut txt = TextEditor::new("ab\ncd"); + txt.move_to_tail(); + + assert!(!txt.move_down()); + } } - #[test] - fn moves_vertically_using_wide_character_display_widths() { - let mut txt = TextEditor::new("界a\n123"); - assert!(txt.move_to(1)); // Display column 2 after `界`. + mod move_up { + use super::*; - assert!(txt.move_down()); + #[test] + fn stops_at_the_document_head() { + let mut txt = TextEditor::new("ab\ncd"); + txt.move_to_head(); - assert_eq!(5, txt.position()); - assert_eq!(TextPosition { row: 1, column: 2 }, txt.logical_position()); + assert!(!txt.move_up()); + assert_eq!(0, txt.position()); + + assert!(txt.move_to(3)); + assert!(txt.move_up()); + assert_eq!(0, txt.position()); + assert!(!txt.move_up()); + } } - #[test] - fn stops_vertical_movement_at_document_boundaries() { - let mut txt = TextEditor::new("ab\ncd"); - txt.move_to_head(); + mod move_to_line_head { + use super::*; + + #[test] + fn moves_to_the_current_line_head() { + let mut txt = TextEditor::new("ab\ncd"); + assert!(txt.move_to(4)); // Before `d`. - assert!(!txt.move_up()); - assert_eq!(0, txt.position()); + txt.move_to_line_head(); + assert_eq!(3, txt.position()); + } + } - assert!(txt.move_to(3)); - assert!(txt.move_up()); - assert_eq!(0, txt.position()); - assert!(!txt.move_up()); + mod move_to_line_tail { + use super::*; - txt.move_to_tail(); - assert!(!txt.move_down()); + #[test] + fn moves_to_the_current_line_tail() { + let mut txt = TextEditor::new("ab\ncd"); + assert!(txt.move_to(4)); // Before `d`. + + txt.move_to_line_tail(); + assert_eq!(5, txt.position()); + } } - #[test] - fn moves_to_the_current_line_boundaries() { - let mut txt = TextEditor::new("ab\ncd"); - assert!(txt.move_to(4)); // Before `d`. + mod erase_forward { + use super::*; - txt.move_to_line_head(); - assert_eq!(3, txt.position()); + #[test] + fn erases_a_newline_and_joins_lines() { + let mut txt = TextEditor::new("ab\ncd"); + assert!(txt.move_to(2)); // Before the newline. - txt.move_to_line_tail(); - assert_eq!(5, txt.position()); + txt.erase_forward(); + + assert_eq!("abcd", txt.text_without_cursor().to_string()); + assert_eq!(2, txt.position()); + assert_eq!(TextPosition { row: 0, column: 2 }, txt.logical_position()); + } } - #[test] - fn erases_a_newline_forward_and_joins_lines() { - let mut txt = TextEditor::new("ab\ncd"); - assert!(txt.move_to(2)); // Before the newline. + mod overwrite { + use super::*; + + #[test] + fn inserts_into_an_empty_editor() { + let mut txt = TextEditor::default(); + let new = new_with_position( + String::from("d "), + 1, // indicate tail. + ); + txt.overwrite('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } - txt.erase_forward(); + #[test] + fn replaces_the_character_at_the_cursor() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("adc "), + 2, // indicate `c`. + ); + txt.overwrite('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn appends_at_the_tail() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + let new = new_with_position( + String::from("abcd "), + 4, // indicate tail. + ); + txt.overwrite('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } - assert_eq!("abcd", txt.text_without_cursor().to_string()); - assert_eq!(2, txt.position()); - assert_eq!(TextPosition { row: 0, column: 2 }, txt.logical_position()); + #[test] + fn replaces_the_first_character_at_the_head() { + let mut txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + let new = new_with_position( + String::from("dbc "), + 1, // indicate `b`. + ); + txt.overwrite('d'); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } } - #[test] - fn handles_empty_lines_and_a_trailing_newline() { - let mut txt = TextEditor::new("a\n\n"); + mod backward { + use super::*; - assert_eq!(TextPosition { row: 2, column: 0 }, txt.logical_position()); - assert!(txt.move_up()); - assert_eq!(TextPosition { row: 1, column: 0 }, txt.logical_position()); - assert!(txt.move_up()); - assert_eq!(TextPosition { row: 0, column: 0 }, txt.logical_position()); + #[test] + fn empty_editor_is_unchanged() { + let mut txt = TextEditor::default(); + txt.backward(); + assert_eq!(StyledGraphemes::from(" "), txt.text()); + assert_eq!(0, txt.position()); + } + + #[test] + fn moves_back_one_character() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + txt.backward(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn moves_back_from_the_tail() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + let new = new_with_position( + String::from("abc "), + 2, // indicate `c`. + ); + txt.backward(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn head_is_unchanged() { + let mut txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + txt.backward(); + assert_eq!(StyledGraphemes::from("abc "), txt.text()); + assert_eq!(0, txt.position()); + } } - } - mod overwrite { - use super::*; + mod forward { + use super::*; - #[test] - fn test_for_empty() { - let mut txt = TextEditor::default(); - let new = new_with_position( - String::from("d "), - 1, // indicate tail. - ); - txt.overwrite('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("adc "), - 2, // indicate `c`. - ); - txt.overwrite('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - let new = new_with_position( - String::from("abcd "), - 4, // indicate tail. - ); - txt.overwrite('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_head() { - let mut txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - let new = new_with_position( - String::from("dbc "), - 1, // indicate `b`. - ); - txt.overwrite('d'); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - } - - mod backward { - use super::*; + #[test] + fn empty_editor_is_unchanged() { + let mut txt = TextEditor::default(); + txt.forward(); + assert_eq!(StyledGraphemes::from(" "), txt.text()); + assert_eq!(0, txt.position()); + } - #[test] - fn test_for_empty() { - let mut txt = TextEditor::default(); - txt.backward(); - assert_eq!(StyledGraphemes::from(" "), txt.text()); - assert_eq!(0, txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - txt.backward(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - let new = new_with_position( - String::from("abc "), - 2, // indicate `c`. - ); - txt.backward(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_head() { - let mut txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - txt.backward(); - assert_eq!(StyledGraphemes::from("abc "), txt.text()); - assert_eq!(0, txt.position()); - } - } - - mod forward { - use super::*; + #[test] + fn moves_forward_one_character() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("abc "), + 2, // indicate `c`. + ); + txt.forward(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } - #[test] - fn test_for_empty() { - let mut txt = TextEditor::default(); - txt.forward(); - assert_eq!(StyledGraphemes::from(" "), txt.text()); - assert_eq!(0, txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("abc "), - 2, // indicate `c`. - ); - txt.forward(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - txt.forward(); - assert_eq!(StyledGraphemes::from("abc "), txt.text()); - assert_eq!(3, txt.position()); - } - - #[test] - fn test_at_head() { - let mut txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - let new = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - txt.forward(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - } - - mod to_head { - use super::*; + #[test] + fn tail_is_unchanged() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + txt.forward(); + assert_eq!(StyledGraphemes::from("abc "), txt.text()); + assert_eq!(3, txt.position()); + } - #[test] - fn test_for_empty() { - let mut txt = TextEditor::default(); - txt.move_to_head(); - assert_eq!(StyledGraphemes::from(" "), txt.text()); - assert_eq!(0, txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - txt.move_to_head(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - let new = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - txt.move_to_head(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_head() { - let mut txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - txt.move_to_head(); - assert_eq!(StyledGraphemes::from("abc "), txt.text()); - assert_eq!(0, txt.position()); - } - } - - mod to_tail { - use super::*; + #[test] + fn moves_forward_from_the_head() { + let mut txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + let new = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + txt.forward(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + } + + mod move_to_head { + use super::*; + + #[test] + fn empty_editor_is_unchanged() { + let mut txt = TextEditor::default(); + txt.move_to_head(); + assert_eq!(StyledGraphemes::from(" "), txt.text()); + assert_eq!(0, txt.position()); + } + + #[test] + fn moves_to_the_head() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + txt.move_to_head(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } - #[test] - fn test_for_empty() { - let mut txt = TextEditor::default(); - txt.move_to_tail(); - assert_eq!(StyledGraphemes::from(" "), txt.text()); - assert_eq!(0, txt.position()); - } - - #[test] - fn test_at_non_edge() { - let mut txt = new_with_position( - String::from("abc "), - 1, // indicate `b`. - ); - let new = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - txt.move_to_tail(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); - } - - #[test] - fn test_at_tail() { - let mut txt = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - txt.move_to_tail(); - assert_eq!(StyledGraphemes::from("abc "), txt.text()); - assert_eq!(3, txt.position()); - } - - #[test] - fn test_at_head() { - let mut txt = new_with_position( - String::from("abc "), - 0, // indicate `a`. - ); - let new = new_with_position( - String::from("abc "), - 3, // indicate tail. - ); - txt.move_to_tail(); - assert_eq!(new.text(), txt.text()); - assert_eq!(new.position(), txt.position()); + #[test] + fn moves_from_the_tail_to_the_head() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + let new = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + txt.move_to_head(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn head_is_unchanged() { + let mut txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + txt.move_to_head(); + assert_eq!(StyledGraphemes::from("abc "), txt.text()); + assert_eq!(0, txt.position()); + } + } + + mod move_to_tail { + use super::*; + + #[test] + fn empty_editor_is_unchanged() { + let mut txt = TextEditor::default(); + txt.move_to_tail(); + assert_eq!(StyledGraphemes::from(" "), txt.text()); + assert_eq!(0, txt.position()); + } + + #[test] + fn moves_to_the_tail() { + let mut txt = new_with_position( + String::from("abc "), + 1, // indicate `b`. + ); + let new = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + txt.move_to_tail(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } + + #[test] + fn tail_is_unchanged() { + let mut txt = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + txt.move_to_tail(); + assert_eq!(StyledGraphemes::from("abc "), txt.text()); + assert_eq!(3, txt.position()); + } + + #[test] + fn moves_from_the_head_to_the_tail() { + let mut txt = new_with_position( + String::from("abc "), + 0, // indicate `a`. + ); + let new = new_with_position( + String::from("abc "), + 3, // indicate tail. + ); + txt.move_to_tail(); + assert_eq!(new.text(), txt.text()); + assert_eq!(new.position(), txt.position()); + } } } } diff --git a/promkit-widgets/tests/jsonz/create_rows.rs b/promkit-widgets/tests/jsonz/create_rows.rs index 00d3fdb9..e371abf6 100644 --- a/promkit-widgets/tests/jsonz/create_rows.rs +++ b/promkit-widgets/tests/jsonz/create_rows.rs @@ -5,7 +5,7 @@ use serde_json::Deserializer; use promkit_widgets::json::jsonz::*; #[test] -fn test_empty_containers() { +fn creates_rows_for_empty_containers() { let values: Vec<_> = Deserializer::from_str( r#" {} @@ -43,7 +43,7 @@ fn test_empty_containers() { } #[test] -fn test_nested_object() { +fn creates_rows_for_a_nested_object() { let input = serde_json::Value::from_str( r#" { @@ -148,7 +148,7 @@ fn test_nested_object() { } #[test] -fn test_nested_array() { +fn creates_rows_for_a_nested_array() { let input = serde_json::Value::from_str( r#" [ @@ -257,7 +257,7 @@ fn test_nested_array() { } #[test] -fn test_mixed_containers() { +fn creates_rows_for_mixed_containers() { let input = serde_json::Value::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/down.rs b/promkit-widgets/tests/jsonz/down.rs index fa6cddc9..82709e5d 100644 --- a/promkit-widgets/tests/jsonz/down.rs +++ b/promkit-widgets/tests/jsonz/down.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use promkit_widgets::json::jsonz::*; #[test] -fn test_collapsed_containers() { +fn skips_collapsed_container_contents() { let input = serde_json::Value::from_str( r#" { @@ -31,7 +31,7 @@ fn test_collapsed_containers() { } #[test] -fn test_down_on_last_collapsed() { +fn stays_on_the_last_collapsed_container() { let input = serde_json::Value::from_str( r#" [ diff --git a/promkit-widgets/tests/jsonz/extract.rs b/promkit-widgets/tests/jsonz/extract.rs index 474dbaed..9f955eb1 100644 --- a/promkit-widgets/tests/jsonz/extract.rs +++ b/promkit-widgets/tests/jsonz/extract.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use promkit_widgets::json::jsonz::*; #[test] -fn test_basic_extract() { +fn extracts_visible_rows() { let input = serde_json::Value::from_str( r#" { @@ -61,7 +61,7 @@ fn test_basic_extract() { } #[test] -fn test_extract_with_collapsed_open() { +fn extracts_a_collapsed_open_container() { let input = serde_json::Value::from_str( r#" { @@ -120,7 +120,7 @@ fn test_extract_with_collapsed_open() { } #[test] -fn test_extract_nested_structure() { +fn extracts_a_nested_structure() { let input = serde_json::Value::from_str( r#" { @@ -176,7 +176,7 @@ fn test_extract_nested_structure() { } #[test] -fn test_extract_boundary_cases() { +fn handles_boundaries() { let input = serde_json::Value::from_str( r#" { @@ -200,7 +200,7 @@ fn test_extract_boundary_cases() { } #[test] -fn test_extract_complex_nested_collapsed() { +fn extracts_complex_nested_collapsed_containers() { let input = serde_json::Value::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/get_all_paths.rs b/promkit-widgets/tests/jsonz/get_all_paths.rs index a41172a6..0938124e 100644 --- a/promkit-widgets/tests/jsonz/get_all_paths.rs +++ b/promkit-widgets/tests/jsonz/get_all_paths.rs @@ -5,7 +5,7 @@ use serde_json::Deserializer; use promkit_widgets::json::jsonz::*; #[test] -fn test_get_all_paths() { +fn returns_all_paths() { let v = serde_json::Value::from_str( r#" { @@ -101,7 +101,7 @@ fn test_get_all_paths() { } #[test] -fn test_get_all_paths_for_jsonl() { +fn returns_all_paths_for_json_lines() { let binding = Deserializer::from_str( r#" {"user": "alice", "age": 30, "hobbies": ["reading", "gaming"]} diff --git a/promkit-widgets/tests/jsonz/head.rs b/promkit-widgets/tests/jsonz/head.rs index e37c57f0..476a8b8b 100644 --- a/promkit-widgets/tests/jsonz/head.rs +++ b/promkit-widgets/tests/jsonz/head.rs @@ -5,7 +5,7 @@ use serde_json::Deserializer; use promkit_widgets::json::jsonz::*; #[test] -fn test_head_after_toggle() { +fn remains_on_the_first_row_after_toggle() { let input = serde_json::Value::from_str( r#" { @@ -29,7 +29,7 @@ fn test_head_after_toggle() { } #[test] -fn test_jsonl() { +fn remains_on_the_first_json_lines_document() { let inputs: Vec<_> = Deserializer::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/multi_documents.rs b/promkit-widgets/tests/jsonz/multi_documents.rs index 2412a9d8..4b17d7de 100644 --- a/promkit-widgets/tests/jsonz/multi_documents.rs +++ b/promkit-widgets/tests/jsonz/multi_documents.rs @@ -3,7 +3,7 @@ use serde_json::Deserializer; use promkit_widgets::json::jsonz::*; #[test] -fn test_basic_multi_documents() { +fn creates_rows_for_multiple_documents() { let inputs: Vec<_> = Deserializer::from_str( r#" { @@ -131,7 +131,7 @@ fn test_basic_multi_documents() { } #[test] -fn test_mixed_multi_documents() { +fn creates_rows_for_mixed_documents() { let inputs: Vec<_> = Deserializer::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/render_pretty.rs b/promkit-widgets/tests/jsonz/render_pretty.rs index b62dd657..c78cf9e0 100644 --- a/promkit-widgets/tests/jsonz/render_pretty.rs +++ b/promkit-widgets/tests/jsonz/render_pretty.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use promkit_widgets::json::jsonz::{PrettyRender, create_rows}; #[test] -fn render_pretty() { +fn renders_nested_rows() { let expected = r#" { "array": [ diff --git a/promkit-widgets/tests/jsonz/set_rows_visibility.rs b/promkit-widgets/tests/jsonz/set_rows_visibility.rs index dba561c5..9aeafabf 100644 --- a/promkit-widgets/tests/jsonz/set_rows_visibility.rs +++ b/promkit-widgets/tests/jsonz/set_rows_visibility.rs @@ -3,7 +3,7 @@ use serde_json::Deserializer; use promkit_widgets::json::jsonz::*; #[test] -fn test() { +fn collapses_and_expands_all_containers() { let inputs: Vec<_> = Deserializer::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/tail.rs b/promkit-widgets/tests/jsonz/tail.rs index 95d264e0..61f798a2 100644 --- a/promkit-widgets/tests/jsonz/tail.rs +++ b/promkit-widgets/tests/jsonz/tail.rs @@ -5,7 +5,7 @@ use serde_json::Deserializer; use promkit_widgets::json::jsonz::*; #[test] -fn test_tail_after_toggle() { +fn follows_visibility_after_toggle() { let input = serde_json::Value::from_str( r#" { @@ -29,7 +29,7 @@ fn test_tail_after_toggle() { } #[test] -fn test_jsonl() { +fn selects_the_last_visible_json_lines_document() { let inputs: Vec<_> = Deserializer::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/toggle.rs b/promkit-widgets/tests/jsonz/toggle.rs index fc5a4707..5d80cb7c 100644 --- a/promkit-widgets/tests/jsonz/toggle.rs +++ b/promkit-widgets/tests/jsonz/toggle.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use promkit_widgets::json::jsonz::*; #[test] -fn test_on_open() { +fn collapses_open_containers() { let input = serde_json::Value::from_str( r#" { @@ -135,7 +135,7 @@ fn test_on_open() { } #[test] -fn test_on_close() { +fn expands_collapsed_containers() { let input = serde_json::Value::from_str( r#" { diff --git a/promkit-widgets/tests/jsonz/up.rs b/promkit-widgets/tests/jsonz/up.rs index 87517420..e8726697 100644 --- a/promkit-widgets/tests/jsonz/up.rs +++ b/promkit-widgets/tests/jsonz/up.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use promkit_widgets::json::jsonz::*; #[test] -fn test_collapsed_containers() { +fn skips_collapsed_container_contents() { let input = serde_json::Value::from_str( r#" { diff --git a/promkit-widgets/tests/yamlz/multi_documents.rs b/promkit-widgets/tests/yamlz/multi_documents.rs index 64791b7b..f3784db0 100644 --- a/promkit-widgets/tests/yamlz/multi_documents.rs +++ b/promkit-widgets/tests/yamlz/multi_documents.rs @@ -8,7 +8,7 @@ fn number(raw: &str) -> serde_yaml::Number { } #[test] -fn test_all_cases_yaml_multi_documents_rows() { +fn creates_rows_for_all_document_shapes() { let input = r#" --- null_value: null diff --git a/promkit/src/terminal_session.rs b/promkit/src/terminal_session.rs index 2ddd6425..aaa213ec 100644 --- a/promkit/src/terminal_session.rs +++ b/promkit/src/terminal_session.rs @@ -218,151 +218,167 @@ mod tests { TerminalSession::try_new_with(mock_backend, modes) } - #[test] - fn restores_fullscreen_modes_in_reverse_order() { - reset([]); - - let modes = TerminalModes::RAW_MODE - | TerminalModes::ALTERNATE_SCREEN - | TerminalModes::HIDDEN_CURSOR - | TerminalModes::MOUSE_CAPTURE; - let mut session = try_new(modes).unwrap(); - session.restore().unwrap(); - session.restore().unwrap(); - - assert_eq!( - operations(), - [ - Operation::EnableRawMode, - Operation::EnterAlternateScreen, - Operation::HideCursor, - Operation::EnableMouseCapture, - Operation::DisableMouseCapture, - Operation::ShowCursor, - Operation::LeaveAlternateScreen, - Operation::DisableRawMode, - ] - ); - } + mod terminal_session { + use super::*; + + mod restore { + use super::*; + + #[test] + fn restores_fullscreen_modes_in_reverse_order() { + reset([]); + + let modes = TerminalModes::RAW_MODE + | TerminalModes::ALTERNATE_SCREEN + | TerminalModes::HIDDEN_CURSOR + | TerminalModes::MOUSE_CAPTURE; + let mut session = try_new(modes).unwrap(); + session.restore().unwrap(); + session.restore().unwrap(); + + assert_eq!( + operations(), + [ + Operation::EnableRawMode, + Operation::EnterAlternateScreen, + Operation::HideCursor, + Operation::EnableMouseCapture, + Operation::DisableMouseCapture, + Operation::ShowCursor, + Operation::LeaveAlternateScreen, + Operation::DisableRawMode, + ] + ); + } - #[test] - fn restores_terminal_when_dropped() { - reset([]); + #[test] + fn continues_after_an_error_and_retries_failed_steps() { + reset([Operation::DisableMouseCapture]); + + let modes = TerminalModes::RAW_MODE + | TerminalModes::ALTERNATE_SCREEN + | TerminalModes::HIDDEN_CURSOR + | TerminalModes::MOUSE_CAPTURE; + let mut session = try_new(modes).unwrap(); + assert_eq!(session.restore().unwrap_err().kind(), ErrorKind::Other); + session.restore().unwrap(); + + assert_eq!( + operations(), + [ + Operation::EnableRawMode, + Operation::EnterAlternateScreen, + Operation::HideCursor, + Operation::EnableMouseCapture, + Operation::DisableMouseCapture, + Operation::ShowCursor, + Operation::LeaveAlternateScreen, + Operation::DisableRawMode, + Operation::DisableMouseCapture, + ] + ); + } - { - let modes = TerminalModes::RAW_MODE - | TerminalModes::HIDDEN_CURSOR - | TerminalModes::MOUSE_CAPTURE; - let _session = try_new(modes).unwrap(); + #[test] + fn applies_only_requested_modes() { + reset([]); + + let modes = TerminalModes::ALTERNATE_SCREEN | TerminalModes::MOUSE_CAPTURE; + let mut session = try_new(modes).unwrap(); + session.restore().unwrap(); + + assert_eq!( + operations(), + [ + Operation::EnterAlternateScreen, + Operation::EnableMouseCapture, + Operation::DisableMouseCapture, + Operation::LeaveAlternateScreen, + ] + ); + } } - assert_eq!( - operations(), - [ - Operation::EnableRawMode, - Operation::HideCursor, - Operation::EnableMouseCapture, - Operation::DisableMouseCapture, - Operation::ShowCursor, - Operation::DisableRawMode, - ] - ); - } - - #[test] - fn rolls_back_attempted_setup_when_setup_fails() { - reset([Operation::HideCursor]); - - let modes = TerminalModes::RAW_MODE - | TerminalModes::ALTERNATE_SCREEN - | TerminalModes::HIDDEN_CURSOR - | TerminalModes::MOUSE_CAPTURE; - let error = try_new(modes).err().expect("setup must fail"); - - assert_eq!(error.kind(), ErrorKind::Other); - assert_eq!( - operations(), - [ - Operation::EnableRawMode, - Operation::EnterAlternateScreen, - Operation::HideCursor, - Operation::ShowCursor, - Operation::LeaveAlternateScreen, - Operation::DisableRawMode, - ] - ); - } - - #[test] - fn restores_attempted_setup_when_setup_panics() { - reset([]); - - let modes = TerminalModes::RAW_MODE - | TerminalModes::ALTERNATE_SCREEN - | TerminalModes::HIDDEN_CURSOR - | TerminalModes::MOUSE_CAPTURE; - let panic = std::panic::catch_unwind(|| { - TerminalSession::try_new_with(panicking_backend, modes).ok(); - }); - - assert!(panic.is_err()); - assert_eq!( - operations(), - [ - Operation::EnableRawMode, - Operation::EnterAlternateScreen, - Operation::HideCursor, - Operation::ShowCursor, - Operation::LeaveAlternateScreen, - Operation::DisableRawMode, - ] - ); - } - - #[test] - fn continues_restoring_after_an_error_and_retries_failed_steps() { - reset([Operation::DisableMouseCapture]); - - let modes = TerminalModes::RAW_MODE - | TerminalModes::ALTERNATE_SCREEN - | TerminalModes::HIDDEN_CURSOR - | TerminalModes::MOUSE_CAPTURE; - let mut session = try_new(modes).unwrap(); - assert_eq!(session.restore().unwrap_err().kind(), ErrorKind::Other); - session.restore().unwrap(); - - assert_eq!( - operations(), - [ - Operation::EnableRawMode, - Operation::EnterAlternateScreen, - Operation::HideCursor, - Operation::EnableMouseCapture, - Operation::DisableMouseCapture, - Operation::ShowCursor, - Operation::LeaveAlternateScreen, - Operation::DisableRawMode, - Operation::DisableMouseCapture, - ] - ); - } - - #[test] - fn applies_only_requested_modes() { - reset([]); + mod drop { + use super::*; + + #[test] + fn restores_terminal_when_dropped() { + reset([]); + + { + let modes = TerminalModes::RAW_MODE + | TerminalModes::HIDDEN_CURSOR + | TerminalModes::MOUSE_CAPTURE; + let _session = try_new(modes).unwrap(); + } + + assert_eq!( + operations(), + [ + Operation::EnableRawMode, + Operation::HideCursor, + Operation::EnableMouseCapture, + Operation::DisableMouseCapture, + Operation::ShowCursor, + Operation::DisableRawMode, + ] + ); + } + } - let modes = TerminalModes::ALTERNATE_SCREEN | TerminalModes::MOUSE_CAPTURE; - let mut session = try_new(modes).unwrap(); - session.restore().unwrap(); + mod try_new_with { + use super::*; + + #[test] + fn rolls_back_attempted_setup_when_setup_fails() { + reset([Operation::HideCursor]); + + let modes = TerminalModes::RAW_MODE + | TerminalModes::ALTERNATE_SCREEN + | TerminalModes::HIDDEN_CURSOR + | TerminalModes::MOUSE_CAPTURE; + let error = try_new(modes).err().expect("setup must fail"); + + assert_eq!(error.kind(), ErrorKind::Other); + assert_eq!( + operations(), + [ + Operation::EnableRawMode, + Operation::EnterAlternateScreen, + Operation::HideCursor, + Operation::ShowCursor, + Operation::LeaveAlternateScreen, + Operation::DisableRawMode, + ] + ); + } - assert_eq!( - operations(), - [ - Operation::EnterAlternateScreen, - Operation::EnableMouseCapture, - Operation::DisableMouseCapture, - Operation::LeaveAlternateScreen, - ] - ); + #[test] + fn restores_attempted_setup_when_setup_panics() { + reset([]); + + let modes = TerminalModes::RAW_MODE + | TerminalModes::ALTERNATE_SCREEN + | TerminalModes::HIDDEN_CURSOR + | TerminalModes::MOUSE_CAPTURE; + let panic = std::panic::catch_unwind(|| { + TerminalSession::try_new_with(panicking_backend, modes).ok(); + }); + + assert!(panic.is_err()); + assert_eq!( + operations(), + [ + Operation::EnableRawMode, + Operation::EnterAlternateScreen, + Operation::HideCursor, + Operation::ShowCursor, + Operation::LeaveAlternateScreen, + Operation::DisableRawMode, + ] + ); + } + } } }