use super::*;
use crate::app::pointer::Hit;
use ratatui::buffer::Buffer;
fn draw(modal: &mut ExportModal, width: u16, height: u16) -> Buffer {
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
render_export_modal(area, &mut buf, modal, &RenderContext::for_test());
buf
}
fn painted(modal: &mut ExportModal, width: u16, height: u16) -> String {
crate::tests::buffer_lines(&draw(modal, width, height)).join("\n")
}
const FORMAT_Y: u16 = 1;
#[test]
fn format_is_one_row_with_the_chosen_value_tinted() {
let ctx = RenderContext::for_test();
let tint = ctx.highlight_style().bg.expect("the default theme tints");
let mut modal = ExportModal::new();
modal.selected_format = ExportFormat::Tsv;
let buf = draw(&mut modal, MAX_WIDTH, HEIGHT);
let rows = crate::tests::buffer_lines(&buf);
let row = &rows[usize::from(FORMAT_Y)];
assert!(row.contains("Format:"), "{row}");
let mut at = 0;
for name in FORMAT_NAMES {
let found = row[at..].find(name).map(|i| i + at);
assert!(found.is_some(), "{name} after {at} in {row:?}");
at = found.unwrap() + name.len();
}
for line in &rows[usize::from(FORMAT_Y) + 1..] {
assert!(!line.contains("Parquet"), "a second list: {line:?}");
}
let x_of = |name: &str| row[..row.find(name).unwrap()].chars().count() as u16;
let tsv = x_of(" TSV ");
for x in tsv..tsv + 5 {
assert_eq!(buf[(x, FORMAT_Y)].bg, tint, "TSV tinted at {x}");
}
let csv = x_of("CSV");
assert_ne!(buf[(csv, FORMAT_Y)].bg, tint, "CSV is not chosen");
let path_row = &rows[usize::from(FORMAT_Y) + 1];
assert!(path_row.contains("Path:"), "{path_row}");
assert_eq!(
x_of("TSV") - x_of("CSV"),
5,
"padded a cell each side: {row:?}"
);
assert_eq!(x_of("CSV"), 2 + 1 + LABEL_WIDTH, "{row:?}");
}
#[test]
fn compression_shows_its_choices() {
let ctx = RenderContext::for_test();
let tint = ctx.highlight_style().bg.expect("the default theme tints");
let mut modal = ExportModal::new();
modal.selected_format = ExportFormat::Csv;
modal.csv_compression = Some(CompressionFormat::Zstd);
let buf = draw(&mut modal, MAX_WIDTH, HEIGHT);
let rows = crate::tests::buffer_lines(&buf);
let (y, row) = rows
.iter()
.enumerate()
.find(|(_, r)| r.contains("Compression:"))
.expect("the compression row");
for name in COMPRESSION_NAMES {
assert!(row.contains(&format!(" {name} ")), "{name} in {row:?}");
}
let x = row[..row.find(" Zstd ").unwrap()].chars().count() as u16;
assert_eq!(buf[(x + 1, y as u16)].bg, tint, "Zstd chosen: {row:?}");
}
#[test]
fn a_narrow_dialog_shows_the_chosen_format_alone() {
let g = crate::glyphs::get();
let mut modal = ExportModal::new();
modal.selected_format = ExportFormat::Parquet;
let rows = crate::tests::buffer_lines(&draw(&mut modal, 50, HEIGHT));
let row = &rows[usize::from(FORMAT_Y)];
let compact = format!("{} Parquet {}", g.choice_prev, g.choice_next);
assert!(row.contains(&compact), "{row:?}");
assert!(!row.contains("CSV") && !row.contains("Avro"), "{row:?}");
let at = row[..row.find("Parquet").unwrap()].chars().count() as u16;
assert_eq!(at, 2 + 1 + LABEL_WIDTH, "{row:?}");
}
#[test]
fn fields_follow_the_format_under_a_fixed_format_row() {
let mut modal = ExportModal::new();
let shown = |modal: &mut ExportModal| {
let rows = crate::tests::buffer_lines(&draw(modal, MAX_WIDTH, HEIGHT));
assert!(rows[usize::from(FORMAT_Y)].contains("Format:"));
["Delimiter:", "Header:", "Compression:", "Source file:"]
.into_iter()
.filter(|label| rows.iter().any(|row| row.contains(label)))
.collect::<Vec<_>>()
};
assert_eq!(shown(&mut modal), ["Delimiter:", "Header:", "Compression:"]);
modal.selected_format = ExportFormat::Tsv;
assert_eq!(shown(&mut modal), ["Header:", "Compression:"]);
modal.selected_format = ExportFormat::Ndjson;
assert_eq!(shown(&mut modal), ["Compression:"]);
modal.selected_format = ExportFormat::Parquet;
assert!(shown(&mut modal).is_empty());
modal.offer_source_file = true;
assert_eq!(shown(&mut modal), ["Source file:"]);
}
#[test]
fn the_dialog_shields_what_it_covers() {
let mut modal = ExportModal::new();
let hits = crate::app::pointer::recording(|| {
draw(&mut modal, MAX_WIDTH, HEIGHT);
});
assert_eq!(
hits.first(),
Some(&(Rect::new(0, 0, MAX_WIDTH, HEIGHT), Hit::Modal))
);
assert_eq!(
hits.iter().filter(|(_, hit)| *hit == Hit::Modal).count(),
1,
"recorded once, by FormView"
);
}
#[test]
fn each_format_value_is_a_click_target() {
let mut modal = ExportModal::new();
modal.selected_format = ExportFormat::Tsv;
let hits = crate::app::pointer::recording(|| {
draw(&mut modal, MAX_WIDTH, HEIGHT);
});
let field = Some(FieldId::of::<ExportModal>(ExportFocus::FormatSelector));
let options: Vec<(u16, usize, usize)> = hits
.iter()
.filter_map(|(rect, hit)| match hit {
Hit::Option {
field: f,
index,
current,
} if *f == field => Some((rect.width, *index, *current)),
_ => None,
})
.collect();
let expected: Vec<(u16, usize, usize)> = FORMAT_NAMES
.iter()
.enumerate()
.map(|(i, name)| (name.len() as u16 + 2, i, 1))
.collect();
assert_eq!(options, expected);
let row = hits
.iter()
.position(|(_, hit)| {
*hit == Hit::Field(FieldId::of::<ExportModal>(ExportFocus::FormatSelector))
})
.expect("the row is recorded");
let first_option = hits
.iter()
.position(|(_, hit)| matches!(hit, Hit::Option { .. }))
.unwrap();
assert!(row < first_option, "the values lie on top of the row");
let hits = crate::app::pointer::recording(|| {
draw(&mut modal, 50, HEIGHT);
});
let steps: Vec<(usize, usize)> = hits
.iter()
.filter_map(|(_, hit)| match hit {
Hit::Option {
field: f,
index,
current,
} if *f == field => Some((*index, *current)),
_ => None,
})
.collect();
assert_eq!(steps, [(0, 1), (1, 0)]);
}
#[test]
fn csv_says_how_nested_columns_are_written() {
let mut modal = ExportModal::new();
for width in [50u16, MAX_WIDTH] {
let out = painted(&mut modal, width, HEIGHT);
assert!(!out.contains(NESTED_NOTE), "no nested columns: {out}");
modal.nested_columns = true;
let out = painted(&mut modal, width, HEIGHT);
assert!(out.contains(NESTED_NOTE), "missing at {width}: {out}");
modal.selected_format = ExportFormat::Parquet;
let out = painted(&mut modal, width, HEIGHT);
assert!(!out.contains(NESTED_NOTE), "Parquet keeps them: {out}");
modal.selected_format = ExportFormat::Csv;
modal.nested_columns = false;
}
}
#[test]
fn avro_says_it_renames_columns() {
let mut modal = ExportModal::new();
modal.selected_format = ExportFormat::Avro;
for width in [50u16, MAX_WIDTH] {
let out = painted(&mut modal, width, HEIGHT);
assert!(!out.contains(AVRO_NAMES_NOTE), "valid names: {out}");
modal.avro_renames = true;
let out = painted(&mut modal, width, HEIGHT);
assert!(out.contains(AVRO_NAMES_NOTE), "missing at {width}: {out}");
modal.selected_format = ExportFormat::Parquet;
let out = painted(&mut modal, width, HEIGHT);
assert!(!out.contains(AVRO_NAMES_NOTE), "Parquet keeps them: {out}");
modal.selected_format = ExportFormat::Avro;
modal.avro_renames = false;
}
}
#[test]
fn the_blank_path_message_renders_inline() {
let mut modal = ExportModal::new();
modal.path_error = Some("Enter a file path.".to_string());
for format in [ExportFormat::Csv, ExportFormat::Parquet] {
modal.selected_format = format;
for width in [50u16, MAX_WIDTH] {
let rows = crate::tests::buffer_lines(&draw(&mut modal, width, HEIGHT));
let at = rows
.iter()
.position(|row| row.contains("Enter a file path."));
assert_eq!(at, Some(usize::from(HEIGHT) - 4), "{format:?} at {width}");
}
}
}
#[test]
fn the_dialog_fits_every_format_at_eighty_columns() {
for (width, full) in [(80u16, true), (60, false)] {
let screen = Rect::new(0, 0, width, 24);
let dialog = dialog_area(screen);
let mut modal = ExportModal::new();
let mut buf = Buffer::empty(screen);
render_export_modal(dialog, &mut buf, &mut modal, &RenderContext::for_test());
let text = crate::tests::buffer_lines(&buf).join("\n");
assert_eq!(text.contains("Avro"), full, "at {width}: {text}");
}
}