use super::reporter::Reporter;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use tsz_checker::diagnostics::Diagnostic;
struct TempDir {
path: PathBuf,
}
impl TempDir {
fn new() -> std::io::Result<Self> {
let mut path = std::env::temp_dir();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
path.push(format!(
"tsz_cli_reporter_test_{}_{}",
std::process::id(),
nanos
));
std::fs::create_dir_all(&path)?;
Ok(Self { path })
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn write_file(path: &Path, contents: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("failed to create parent directory");
}
std::fs::write(path, contents).expect("failed to write file");
}
#[test]
fn plain_mode_formats_diagnostic_with_location() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("src/main.ts");
write_file(&file_path, "let x = 1;\nlet y = 2;\n");
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
11,
1,
"Cannot find name 'y'.".to_string(),
2304,
);
let mut reporter = Reporter::new(false);
let output = reporter.render(std::slice::from_ref(&diagnostic));
let expected = format!(
"{}(2,1): error TS2304: Cannot find name 'y'.\n",
diagnostic.file
);
assert_eq!(output, expected);
}
#[test]
fn plain_mode_omits_code_when_zero() {
let diagnostic =
Diagnostic::error("missing.ts".to_string(), 0, 0, "Parse error".to_string(), 0);
let mut reporter = Reporter::new(false);
let output = reporter.render(&[diagnostic]);
assert!(output.contains(": error: Parse error"), "{output}");
}
#[test]
fn plain_mode_no_source_snippets() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(&file_path, "let x: number = \"string\";\n");
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
16,
8,
"Type 'string' is not assignable to type 'number'.".to_string(),
2322,
);
let mut reporter = Reporter::new(false);
let output = reporter.render(&[diagnostic]);
assert!(
!output.contains("let x: number"),
"non-pretty should not include source snippets: {output}"
);
assert!(
!output.contains('~'),
"non-pretty should not include underline: {output}"
);
assert_eq!(
output.lines().count(),
1,
"non-pretty should be single line: {output}"
);
}
#[test]
fn plain_mode_multiple_diagnostics() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(
&file_path,
"let x: number = \"string\";\nlet y: string = 42;\n",
);
let diagnostics = vec![
Diagnostic::error(
file_path.to_string_lossy().into_owned(),
4,
1,
"Type 'string' is not assignable to type 'number'.".to_string(),
2322,
),
Diagnostic::error(
file_path.to_string_lossy().into_owned(),
30,
1,
"Type 'number' is not assignable to type 'string'.".to_string(),
2322,
),
];
let mut reporter = Reporter::new(false);
let output = reporter.render(&diagnostics);
assert_eq!(output.lines().count(), 2, "expected 2 lines: {output}");
for line in output.lines() {
assert!(!line.contains('~'), "non-pretty should not have underlines");
}
}
#[test]
fn pretty_mode_uses_colon_separated_location() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(&file_path, "let x = 1;\nlet y = 2;\n");
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
11,
1,
"Cannot find name 'y'.".to_string(),
2304,
);
let mut reporter = Reporter::new(false);
reporter.set_pretty(true);
let output = reporter.render(std::slice::from_ref(&diagnostic));
assert!(
output.contains(&format!("{}:2:1 - error TS2304: ", diagnostic.file)),
"pretty mode should use colon-separated location: {output}"
);
}
#[test]
fn pretty_mode_includes_source_snippet() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(&file_path, "let x: number = \"string\";\n");
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
16,
8,
"Type 'string' is not assignable to type 'number'.".to_string(),
2322,
);
let mut reporter = Reporter::new(false);
reporter.set_pretty(true);
let output = reporter.render(&[diagnostic]);
assert!(
output.contains("1 let x: number = \"string\";"),
"missing source line: {output}"
);
assert!(output.contains("~~~~~~~~"), "missing underline: {output}");
}
#[test]
fn pretty_mode_snippet_line_number_alignment() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
let mut source = String::new();
for i in 1..=9 {
source.push_str(&format!("let a{i} = {i};\n"));
}
source.push_str("let a10: number = \"string\";\n");
write_file(&file_path, &source);
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
source.find("\"string\"").unwrap() as u32,
8,
"Type 'string' is not assignable to type 'number'.".to_string(),
2322,
);
let mut reporter = Reporter::new(false);
reporter.set_pretty(true);
let output = reporter.render(&[diagnostic]);
assert!(
output.contains("10 let a10: number = \"string\";"),
"missing line 10 source: {output}"
);
}
#[test]
fn pretty_mode_summary_single_error_single_file() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(&file_path, "let x = unknownVar;\n");
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
8,
10,
"Cannot find name 'unknownVar'.".to_string(),
2304,
);
let mut reporter = Reporter::new(false);
reporter.set_pretty(true);
let output = reporter.render(std::slice::from_ref(&diagnostic));
assert!(
output.contains("Found 1 error in"),
"missing summary line: {output}"
);
}
#[test]
fn pretty_mode_summary_multiple_errors_same_file() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(&file_path, "let x = a;\nlet y = b;\n");
let diagnostics = vec![
Diagnostic::error(
file_path.to_string_lossy().into_owned(),
8,
1,
"Cannot find name 'a'.".to_string(),
2304,
),
Diagnostic::error(
file_path.to_string_lossy().into_owned(),
19,
1,
"Cannot find name 'b'.".to_string(),
2304,
),
];
let mut reporter = Reporter::new(false);
reporter.set_pretty(true);
let output = reporter.render(&diagnostics);
assert!(
output.contains("Found 2 errors in the same file, starting at:"),
"missing multi-error summary: {output}"
);
}
#[test]
fn pretty_mode_has_blank_line_between_header_and_snippet() {
let temp = TempDir::new().expect("temp dir");
let file_path = temp.path.join("test.ts");
write_file(&file_path, "let x = unknownVar;\n");
let diagnostic = Diagnostic::error(
file_path.to_string_lossy().into_owned(),
8,
10,
"Cannot find name 'unknownVar'.".to_string(),
2304,
);
let mut reporter = Reporter::new(false);
reporter.set_pretty(true);
let output = reporter.render(&[diagnostic]);
let lines: Vec<&str> = output.lines().collect();
assert!(lines.len() >= 4, "expected at least 4 lines: {output}");
assert!(
lines[1].is_empty(),
"expected blank line between header and source, got: '{}'",
lines[1]
);
}