use super::*;
use ansi_escape_sequences::has_ansi;
// ANSI style definitions for test helpers
#[derive(Debug, Clone)]
struct AnsiStyle {
open: &'static str,
close: &'static str,
}
#[derive(Debug, Clone)]
struct ColorStyles {
red: AnsiStyle,
green: AnsiStyle,
blue: AnsiStyle,
black: AnsiStyle,
}
#[derive(Debug, Clone)]
struct BgColorStyles {
bg_green: AnsiStyle,
bg_red: AnsiStyle,
}
#[derive(Debug, Clone)]
struct AnsiStyles {
color: ColorStyles,
bg_color: BgColorStyles,
}
impl AnsiStyles {
fn new() -> Self {
Self {
color: ColorStyles {
red: AnsiStyle {
open: "\u{001B}[31m",
close: "\u{001B}[39m",
},
green: AnsiStyle {
open: "\u{001B}[32m",
close: "\u{001B}[39m",
},
blue: AnsiStyle {
open: "\u{001B}[34m",
close: "\u{001B}[39m",
},
black: AnsiStyle {
open: "\u{001B}[30m",
close: "\u{001B}[39m",
},
},
bg_color: BgColorStyles {
bg_green: AnsiStyle {
open: "\u{001B}[42m",
close: "\u{001B}[49m",
},
bg_red: AnsiStyle {
open: "\u{001B}[41m",
close: "\u{001B}[49m",
},
},
}
}
}
// Helper function to create colored text like chalk
struct Chalk {
styles: AnsiStyles,
}
impl Chalk {
fn new() -> Self {
Self {
styles: AnsiStyles::new(),
}
}
fn red(&self, text: &str) -> String {
format!(
"{}{}{}",
self.styles.color.red.open, text, self.styles.color.red.close
)
}
fn green(&self, text: &str) -> String {
format!(
"{}{}{}",
self.styles.color.green.open, text, self.styles.color.green.close
)
}
fn blue(&self, text: &str) -> String {
format!(
"{}{}{}",
self.styles.color.blue.open, text, self.styles.color.blue.close
)
}
fn black(&self, text: &str) -> String {
format!(
"{}{}{}",
self.styles.color.black.open, text, self.styles.color.black.close
)
}
fn bg_green(&self, text: &str) -> String {
format!(
"{}{}{}",
self.styles.bg_color.bg_green.open, text, self.styles.bg_color.bg_green.close
)
}
fn bg_red(&self, text: &str) -> String {
format!(
"{}{}{}",
self.styles.bg_color.bg_red.open, text, self.styles.bg_color.bg_red.close
)
}
}
// Test fixtures matching JavaScript tests
fn get_fixture() -> String {
let chalk = Chalk::new();
format!(
"The quick brown {}the lazy {}",
chalk.red("fox jumped over "),
chalk.green("dog and then ran away with the unicorn.")
)
}
const FIXTURE2: &str = "12345678\n901234567890";
const FIXTURE3: &str = "12345678\n901234567890 12345";
const FIXTURE4: &str = "12345678\n";
const FIXTURE5: &str = "12345678\n ";
#[test]
fn test_wraps_string_at_20_characters() {
let fixture = get_fixture();
let result = wrap_ansi(&fixture, 20, None);
let expected = "The quick brown \u{001B}[31mfox\u{001B}[39m\n\u{001B}[31mjumped over \u{001B}[39mthe lazy\n\u{001B}[32mdog and then ran\u{001B}[39m\n\u{001B}[32maway with the\u{001B}[39m\n\u{001B}[32municorn.\u{001B}[39m";
assert_eq!(result, expected);
// Verify all lines are <= 20 characters when ANSI codes are stripped
assert!(strip_ansi(&result).split('\n').all(|line| line.len() <= 20));
}
#[test]
fn test_wraps_string_at_30_characters() {
let fixture = get_fixture();
let result = wrap_ansi(&fixture, 30, None);
let expected = "The quick brown \u{001B}[31mfox jumped\u{001B}[39m\n\u{001B}[31mover \u{001B}[39mthe lazy \u{001B}[32mdog and then ran\u{001B}[39m\n\u{001B}[32maway with the unicorn.\u{001B}[39m";
assert_eq!(result, expected);
// Verify all lines are <= 30 characters when ANSI codes are stripped
assert!(strip_ansi(&result).split('\n').all(|line| line.len() <= 30));
}
#[test]
fn test_does_not_break_strings_longer_than_cols_characters() {
let fixture = get_fixture();
let options = WrapOptions {
hard: false,
..Default::default()
};
let result = wrap_ansi(&fixture, 5, Some(options));
let expected = "The\nquick\nbrown\n\u{001B}[31mfox\u{001B}[39m\n\u{001B}[31mjumped\u{001B}[39m\n\u{001B}[31mover\u{001B}[39m\n\u{001B}[31m\u{001B}[39mthe\nlazy\n\u{001B}[32mdog\u{001B}[39m\n\u{001B}[32mand\u{001B}[39m\n\u{001B}[32mthen\u{001B}[39m\n\u{001B}[32mran\u{001B}[39m\n\u{001B}[32maway\u{001B}[39m\n\u{001B}[32mwith\u{001B}[39m\n\u{001B}[32mthe\u{001B}[39m\n\u{001B}[32municorn.\u{001B}[39m";
assert_eq!(result, expected);
// Verify some lines are longer than 5 characters (soft wrap)
assert!(strip_ansi(&result).split('\n').any(|line| line.len() > 5));
}
#[test]
fn test_handles_colored_string_that_wraps_on_to_multiple_lines() {
let chalk = Chalk::new();
let input = format!("{} hey!", chalk.green("hello world"));
let options = WrapOptions {
hard: false,
..Default::default()
};
let result = wrap_ansi(&input, 5, Some(options));
let lines: Vec<&str> = result.split('\n').collect();
assert!(has_ansi(lines[0]));
assert!(has_ansi(lines[1]));
assert!(!has_ansi(lines[2]));
}
#[test]
fn test_does_not_prepend_newline_if_first_string_is_greater_than_cols() {
let chalk = Chalk::new();
let input = format!("{}-world", chalk.green("hello"));
let options = WrapOptions {
hard: false,
..Default::default()
};
let result = wrap_ansi(&input, 5, Some(options));
assert_eq!(result.split('\n').count(), 1);
}
#[test]
fn test_breaks_strings_longer_than_cols_characters_hard_mode() {
let fixture = get_fixture();
let options = WrapOptions {
hard: true,
..Default::default()
};
let result = wrap_ansi(&fixture, 5, Some(options));
let expected = "The\nquick\nbrown\n\u{001B}[31mfox j\u{001B}[39m\n\u{001B}[31mumped\u{001B}[39m\n\u{001B}[31mover\u{001B}[39m\n\u{001B}[31m\u{001B}[39mthe\nlazy\n\u{001B}[32mdog\u{001B}[39m\n\u{001B}[32mand\u{001B}[39m\n\u{001B}[32mthen\u{001B}[39m\n\u{001B}[32mran\u{001B}[39m\n\u{001B}[32maway\u{001B}[39m\n\u{001B}[32mwith\u{001B}[39m\n\u{001B}[32mthe\u{001B}[39m\n\u{001B}[32munico\u{001B}[39m\n\u{001B}[32mrn.\u{001B}[39m";
assert_eq!(result, expected);
// Verify all lines are <= 5 characters when ANSI codes are stripped
assert!(strip_ansi(&result).split('\n').all(|line| line.len() <= 5));
}
#[test]
fn test_removes_last_row_if_it_contained_only_ansi_escape_codes() {
let chalk = Chalk::new();
let input = chalk.green("helloworld");
let options = WrapOptions {
hard: true,
..Default::default()
};
let result = wrap_ansi(&input, 2, Some(options));
assert!(strip_ansi(&result).split('\n').all(|x| x.len() == 2));
}
#[test]
fn test_does_not_prepend_newline_if_first_word_is_split() {
let chalk = Chalk::new();
let input = format!("{}world", chalk.green("hello"));
let options = WrapOptions {
hard: true,
..Default::default()
};
let result = wrap_ansi(&input, 5, Some(options));
assert_eq!(result.split('\n').count(), 2);
}
#[test]
fn test_takes_into_account_line_returns_inside_input() {
let options = WrapOptions {
hard: true,
..Default::default()
};
let result = wrap_ansi(FIXTURE2, 10, Some(options));
assert_eq!(result, "12345678\n9012345678\n90");
}
#[test]
fn test_word_wrapping() {
let result = wrap_ansi(FIXTURE3, 15, None);
assert_eq!(result, "12345678\n901234567890\n12345");
}
#[test]
fn test_no_word_wrapping() {
let options = WrapOptions {
word_wrap: false,
..Default::default()
};
let result = wrap_ansi(FIXTURE3, 15, Some(options));
assert_eq!(result, "12345678\n901234567890 12\n345");
let result2 = wrap_ansi(FIXTURE3, 5, Some(options));
assert_eq!(result2, "12345\n678\n90123\n45678\n90 12\n345");
let result3 = wrap_ansi(FIXTURE5, 5, Some(options));
assert_eq!(result3, "12345\n678\n");
let fixture = get_fixture();
let result4 = wrap_ansi(&fixture, 5, Some(options));
let expected = "The q\nuick\nbrown\n\u{001B}[31mfox j\u{001B}[39m\n\u{001B}[31mumped\u{001B}[39m\n\u{001B}[31mover\u{001B}[39m\n\u{001B}[31m\u{001B}[39mthe l\nazy \u{001B}[32md\u{001B}[39m\n\u{001B}[32mog an\u{001B}[39m\n\u{001B}[32md the\u{001B}[39m\n\u{001B}[32mn ran\u{001B}[39m\n\u{001B}[32maway\u{001B}[39m\n\u{001B}[32mwith\u{001B}[39m\n\u{001B}[32mthe u\u{001B}[39m\n\u{001B}[32mnicor\u{001B}[39m\n\u{001B}[32mn.\u{001B}[39m";
assert_eq!(result4, expected);
}
#[test]
fn test_no_word_wrapping_and_no_trimming() {
let options = WrapOptions {
word_wrap: false,
trim: false,
hard: false,
};
let result = wrap_ansi(FIXTURE3, 13, Some(options));
assert_eq!(result, "12345678\n901234567890 \n12345");
let result2 = wrap_ansi(FIXTURE4, 5, Some(options));
assert_eq!(result2, "12345\n678\n");
let result3 = wrap_ansi(FIXTURE5, 5, Some(options));
assert_eq!(result3, "12345\n678\n ");
}
#[test]
fn test_supports_fullwidth_characters() {
let options = WrapOptions {
hard: true,
..Default::default()
};
let result = wrap_ansi("안녕하세", 4, Some(options));
assert_eq!(result, "안녕\n하세");
}
#[test]
fn test_supports_unicode_surrogate_pairs() {
let options = WrapOptions {
hard: true,
..Default::default()
};
let result = wrap_ansi("a\u{1F200}bc", 2, Some(options));
assert_eq!(result, "a\n\u{1F200}\nbc");
let result2 = wrap_ansi("a\u{1F200}bc\u{1F200}d\u{1F200}", 2, Some(options));
assert_eq!(result2, "a\n\u{1F200}\nbc\n\u{1F200}\nd\n\u{1F200}");
}
#[test]
fn test_properly_wraps_whitespace_with_no_trimming() {
let options = WrapOptions {
trim: false,
..Default::default()
};
let result = wrap_ansi(" ", 2, Some(options));
assert_eq!(result, " \n ");
let options_hard = WrapOptions {
trim: false,
hard: true,
word_wrap: true,
};
let result2 = wrap_ansi(" ", 2, Some(options_hard));
assert_eq!(result2, " \n ");
}
#[test]
fn test_trims_leading_and_trailing_whitespace_only_on_actual_wrapped_lines() {
let result = wrap_ansi(" foo bar ", 3, None);
assert_eq!(result, "foo\nbar");
let result2 = wrap_ansi(" foo bar ", 6, None);
assert_eq!(result2, "foo\nbar");
let result3 = wrap_ansi(" foo bar ", 42, None);
assert_eq!(result3, "foo bar");
let options = WrapOptions {
trim: false,
hard: false,
word_wrap: true,
};
let result4 = wrap_ansi(" foo bar ", 42, Some(options));
assert_eq!(result4, " foo bar ");
}
#[test]
fn test_trims_leading_and_trailing_whitespace_inside_color_block() {
let chalk = Chalk::new();
let input = chalk.blue(" foo bar ");
let result = wrap_ansi(&input, 6, None);
let expected = format!(
"{}\n{}",
"\u{001B}[34mfoo\u{001B}[39m", "\u{001B}[34mbar\u{001B}[39m"
);
assert_eq!(result, expected);
let result2 = wrap_ansi(&input, 42, None);
let expected2 = chalk.blue("foo bar");
assert_eq!(result2, expected2);
let options_no_trim = WrapOptions {
trim: false,
hard: false,
word_wrap: true,
};
let result3 = wrap_ansi(&input, 42, Some(options_no_trim));
let expected3 = chalk.blue(" foo bar ");
assert_eq!(result3, expected3);
}
#[test]
fn test_properly_wraps_whitespace_between_words_with_no_trimming() {
let result1 = wrap_ansi("foo bar", 3, None);
assert_eq!(result1, "foo\nbar");
let options_hard = WrapOptions {
hard: true,
..Default::default()
};
let result2 = wrap_ansi("foo bar", 3, Some(options_hard));
assert_eq!(result2, "foo\nbar");
let options_no_trim = WrapOptions {
trim: false,
..Default::default()
};
let result3 = wrap_ansi("foo bar", 3, Some(options_no_trim));
assert_eq!(result3, "foo\n \nbar");
let options_no_trim_hard = WrapOptions {
trim: false,
hard: true,
word_wrap: true,
};
let result4 = wrap_ansi("foo bar", 3, Some(options_no_trim_hard));
assert_eq!(result4, "foo\n \nbar");
}
#[test]
fn test_does_not_multiplicate_leading_spaces_with_no_trimming() {
let options = WrapOptions {
trim: false,
..Default::default()
};
let result1 = wrap_ansi(" a ", 10, Some(options));
assert_eq!(result1, " a ");
let result2 = wrap_ansi(" a ", 10, Some(options));
assert_eq!(result2, " a ");
}
#[test]
fn test_does_not_remove_spaces_in_line_with_ansi_escapes_when_no_trimming() {
let chalk = Chalk::new();
let options = WrapOptions {
trim: false,
..Default::default()
};
let input1 = chalk.bg_green(&format!(" {} ", chalk.black("OK")));
let result1 = wrap_ansi(&input1, 100, Some(options));
let expected1 = chalk.bg_green(&format!(" {} ", chalk.black("OK")));
assert_eq!(result1, expected1);
let input2 = chalk.bg_green(&format!(" {} ", chalk.black("OK")));
let result2 = wrap_ansi(&input2, 100, Some(options));
let expected2 = chalk.bg_green(&format!(" {} ", chalk.black("OK")));
assert_eq!(result2, expected2);
let options_hard_no_trim = WrapOptions {
hard: true,
trim: false,
word_wrap: true,
};
let input3 = chalk.bg_green(" hello ");
let result3 = wrap_ansi(&input3, 10, Some(options_hard_no_trim));
let expected3 = chalk.bg_green(" hello ");
assert_eq!(result3, expected3);
}
#[test]
fn test_wraps_hyperlinks_preserving_clickability() {
let options = WrapOptions {
hard: true,
..Default::default()
};
let input1 = "Check out \u{001B}]8;;https://www.example.com\u{0007}my website\u{001B}]8;;\u{0007}, it is \u{001B}]8;;https://www.example.com\u{0007}supercalifragilisticexpialidocious\u{001B}]8;;\u{0007}.";
let result1 = wrap_ansi(input1, 16, Some(options));
let expected1 = "Check out \u{001B}]8;;https://www.example.com\u{0007}my\u{001B}]8;;\u{0007}\n\u{001B}]8;;https://www.example.com\u{0007}website\u{001B}]8;;\u{0007}, it is\n\u{001B}]8;;https://www.example.com\u{0007}supercalifragili\u{001B}]8;;\u{0007}\n\u{001B}]8;;https://www.example.com\u{0007}sticexpialidocio\u{001B}]8;;\u{0007}\n\u{001B}]8;;https://www.example.com\u{0007}us\u{001B}]8;;\u{0007}.";
assert_eq!(result1, expected1);
let chalk = Chalk::new();
let input2 = format!("Check out \u{001B}]8;;https://www.example.com\u{0007}my \u{1F200} {}\u{001B}]8;;\u{0007}, it {}.",
chalk.bg_green("website"),
chalk.bg_red("is \u{001B}]8;;https://www.example.com\u{0007}super\u{1F200}califragilisticexpialidocious\u{001B}]8;;\u{0007}"));
let result2 = wrap_ansi(&input2, 16, Some(options));
let expected2 = "Check out \u{001B}]8;;https://www.example.com\u{0007}my 🈀\u{001B}]8;;\u{0007}\n\u{001B}]8;;https://www.example.com\u{0007}\u{001B}[42mwebsite\u{001B}[49m\u{001B}]8;;\u{0007}, it \u{001B}[41mis\u{001B}[49m\n\u{001B}[41m\u{001B}]8;;https://www.example.com\u{0007}super🈀califragi\u{001B}]8;;\u{0007}\u{001B}[49m\n\u{001B}[41m\u{001B}]8;;https://www.example.com\u{0007}listicexpialidoc\u{001B}]8;;\u{0007}\u{001B}[49m\n\u{001B}[41m\u{001B}]8;;https://www.example.com\u{0007}ious\u{001B}]8;;\u{0007}\u{001B}[49m.";
assert_eq!(result2, expected2);
}
#[test]
fn test_covers_non_sgr_non_hyperlink_ansi_escapes() {
let result1 = wrap_ansi("Hello, \u{001B}[1D World!", 8, None);
assert_eq!(result1, "Hello,\u{001B}[1D\nWorld!");
let options = WrapOptions {
trim: false,
..Default::default()
};
let result2 = wrap_ansi("Hello, \u{001B}[1D World!", 8, Some(options));
assert_eq!(result2, "Hello, \u{001B}[1D \nWorld!");
}
#[test]
fn test_normalizes_newlines() {
let options = WrapOptions {
hard: true,
..Default::default()
};
let result1 = wrap_ansi("foobar\r\nfoobar\r\nfoobar\nfoobar", 3, Some(options));
assert_eq!(result1, "foo\nbar\nfoo\nbar\nfoo\nbar\nfoo\nbar");
let result2 = wrap_ansi("foo bar\r\nfoo bar\r\nfoo bar\nfoo bar", 3, None);
assert_eq!(result2, "foo\nbar\nfoo\nbar\nfoo\nbar\nfoo\nbar");
}