use assert_cmd::Command;
use std::fs;
use tempfile::NamedTempFile;
fn mdv_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("mdv"))
}
fn checkbox_markdown() -> String {
[
"- [ ] unchecked",
"- [x] done",
"- [-] canceled",
"- [?] question",
"- [!] important",
"- [/] in progress",
"- [|] alt progress",
"- [\\] backslash state",
]
.join("\n")
+ "\n"
}
fn run(args: &[&str], markdown: &str) -> String {
let temp_file = NamedTempFile::new().unwrap();
fs::write(&temp_file, markdown).unwrap();
let mut cmd = mdv_cmd();
cmd.arg("--no-colors");
for arg in args {
cmd.arg(arg);
}
cmd.arg(temp_file.path());
let output = cmd.output().expect("mdv executed");
assert!(output.status.success(), "mdv failed: {:?}", output.status);
String::from_utf8(output.stdout).expect("stdout utf8")
}
fn run_with_colors(args: &[&str], markdown: &str) -> String {
let temp_file = NamedTempFile::new().unwrap();
fs::write(&temp_file, markdown).unwrap();
let mut cmd = mdv_cmd();
for arg in args {
cmd.arg(arg);
}
cmd.arg(temp_file.path());
let output = cmd.output().expect("mdv executed");
assert!(output.status.success(), "mdv failed: {:?}", output.status);
String::from_utf8(output.stdout).expect("stdout utf8")
}
#[test]
fn test_pretty_checkbox_square_icons() {
let stdout = run(&["--pretty-checkbox", "square"], &checkbox_markdown());
let icons = [
('\u{F0131}', "unchecked"),
('\u{F0132}', "done"),
('\u{F0375}', "canceled"),
('\u{F078B}', "question"),
('\u{F0027}', "important"),
('\u{F0856}', "in progress"),
('\u{F0856}', "alt progress"),
('\u{F0856}', "backslash state"),
];
for (icon, label) in icons {
let line = stdout
.lines()
.find(|l| l.contains(label))
.unwrap_or_else(|| panic!("missing line for {label}"));
assert!(
line.contains(icon),
"square icon {icon:?} not rendered for [{label}] in line: {line:?}"
);
}
}
#[test]
fn test_pretty_checkbox_circle_icons() {
let stdout = run(&["--pretty-checkbox", "circle"], &checkbox_markdown());
let expected = [
('\u{F0130}', "unchecked"),
('\u{F0133}', "done"),
('\u{F0376}', "canceled"),
('\u{F02D7}', "question"),
('\u{F0028}', "important"),
('\u{F0AA2}', "in progress"),
('\u{F0AA1}', "alt progress"),
('\u{F0AA0}', "backslash state"),
];
for (icon, label) in expected {
let line = stdout
.lines()
.find(|l| l.contains(label))
.unwrap_or_else(|| panic!("missing line for {label}"));
assert!(
line.contains(icon),
"circle icon not rendered for [{label}] in line: {line:?}"
);
}
}
#[test]
fn test_custom_checkbox_overrides_default() {
let md = "- [ ] overridden\n";
let stdout = run(
&[
"--pretty-checkbox",
"square",
"--custom-checkbox",
" :\u{F0026}",
],
md,
);
let line = stdout.lines().find(|l| l.contains("overridden")).unwrap();
assert!(line.contains('\u{F0026}'), "override not applied: {line:?}");
assert!(!line.contains('\u{F0130}'), "default icon leaked: {line:?}");
}
#[test]
fn test_custom_checkbox_adds_new_state() {
let md = "- [*] starred\n";
let stdout = run(
&[
"--pretty-checkbox",
"square",
"--custom-checkbox",
"*:\u{F078B}",
],
md,
);
let line = stdout.lines().find(|l| l.contains("starred")).unwrap();
assert!(
line.contains('\u{F078B}'),
"new state not rendered: {line:?}"
);
}
#[test]
fn test_custom_checkbox_ignored_without_pretty() {
let md = "- [*] starred\n- [x] done\n";
let stdout = run(&["--custom-checkbox", "*:\u{F078B}"], md);
let starred = stdout.lines().find(|l| l.contains("starred")).unwrap();
assert!(
starred.contains("[*]"),
"custom state should stay literal without pretty mode: {starred:?}"
);
let done = stdout.lines().find(|l| l.contains("done")).unwrap();
assert!(
done.contains("[✓]"),
"default checked marker changed: {done:?}"
);
}
#[test]
fn test_backslash_checkbox_both_writings() {
let md = "- [\\] single\n- [\\\\] double\n";
let stdout = run(&["--pretty-checkbox", "square"], md);
let single = stdout.lines().find(|l| l.contains("single")).unwrap();
let double = stdout.lines().find(|l| l.contains("double")).unwrap();
assert!(
single.contains('\u{F0856}'),
"single backslash not normalized: {single:?}"
);
assert!(
double.contains('\u{F0856}'),
"double backslash not rendered: {double:?}"
);
}
#[test]
fn test_default_checkbox_unchanged_without_pretty() {
let stdout = run(&[], &checkbox_markdown());
let unchecked = stdout.lines().find(|l| l.contains("unchecked")).unwrap();
assert!(unchecked.contains("[ ]"));
let done = stdout.lines().find(|l| l.contains("done")).unwrap();
assert!(done.contains("[✓]"));
let canceled = stdout.lines().find(|l| l.contains("canceled")).unwrap();
assert!(canceled.contains("[-]"));
}
#[test]
fn test_custom_checkbox_color_override() {
let md = "- [*] starred\n- [!] important\n";
let stdout = run_with_colors(
&[
"--pretty-checkbox",
"square",
"--custom-checkbox",
"*:\u{F078B}:yellow;!:\u{F0027}:128,1,1",
],
md,
);
let starred = stdout.lines().find(|l| l.contains("starred")).unwrap();
assert!(
starred.contains("\x1b[33m")
|| starred.contains("\x1b[93m")
|| starred.contains("\x1b[38;5;3m"),
"yellow color not applied to [*]: {starred:?}"
);
let important = stdout.lines().find(|l| l.contains("important")).unwrap();
assert!(
important.contains("\x1b[38;2;128;1;1m"),
"RGB color not applied to [!]: {important:?}"
);
}
#[test]
fn test_custom_checkbox_hex_color() {
let md = "- [ ] test\n";
let stdout = run_with_colors(
&[
"--pretty-checkbox",
"square",
"--custom-checkbox",
" :\u{F0131}:#ff5500",
],
md,
);
let line = stdout.lines().find(|l| l.contains("test")).unwrap();
assert!(
line.contains("\x1b[38;2;255;85;0m"),
"hex color not applied: {line:?}"
);
}
#[test]
fn test_custom_checkbox_no_color_still_works() {
let md = "- [*] starred\n";
let stdout = run(
&[
"--pretty-checkbox",
"square",
"--custom-checkbox",
"*:\u{F078B}",
],
md,
);
let starred = stdout.lines().find(|l| l.contains("starred")).unwrap();
assert!(
starred.contains('\u{F078B}'),
"icon without color should still render: {starred:?}"
);
}
#[test]
fn test_pretty_checkbox_nested_indent() {
let md = "- [ ] top\n - [x] child\n - [-] deep\n - [?] back\n";
let stdout = run(&["--pretty-checkbox", "square"], md);
let lines: Vec<&str> = stdout.lines().collect();
let top = lines.iter().find(|l| l.contains("top")).unwrap();
let top_indent = top.len() - top.trim_start().len();
let child = lines.iter().find(|l| l.contains("child")).unwrap();
let child_indent = child.len() - child.trim_start().len();
let deep = lines.iter().find(|l| l.contains("deep")).unwrap();
let deep_indent = deep.len() - deep.trim_start().len();
assert!(
top_indent < child_indent,
"child should be more indented than top: top={top_indent} child={child_indent}"
);
assert!(
child_indent < deep_indent,
"deep should be more indented than child: child={child_indent} deep={deep_indent}"
);
let back = lines.iter().find(|l| l.contains("back")).unwrap();
let back_indent = back.len() - back.trim_start().len();
assert_eq!(
back_indent, child_indent,
"back should match child indent: child={child_indent} back={back_indent}"
);
}
#[test]
fn test_pretty_checkbox_heading_indent() {
let md = "# H1\n\n- [ ] under h1\n\n## H2\n\n- [ ] under h2\n";
let stdout = run(&["--pretty-checkbox", "square"], md);
let h1_line = stdout.lines().find(|l| l.contains("under h1")).unwrap();
let h2_line = stdout.lines().find(|l| l.contains("under h2")).unwrap();
let h1_indent = h1_line.len() - h1_line.trim_start().len();
let h2_indent = h2_line.len() - h2_line.trim_start().len();
assert!(
h2_indent > h1_indent,
"H2 checkbox should be more indented than H1: h1={h1_indent} h2={h2_indent}"
);
}
#[test]
fn test_pretty_checkbox_bullet_removed_not_regular_items() {
let md = "- [ ] checkbox item\n- regular item\n";
let stdout = run(&["--pretty-checkbox", "square"], md);
let checkbox_line = stdout
.lines()
.find(|l| l.contains("checkbox item"))
.unwrap();
let regular_line = stdout.lines().find(|l| l.contains("regular item")).unwrap();
let checkbox_stripped = checkbox_line.trim_start();
assert!(
!checkbox_stripped.starts_with("- "),
"bullet should be removed for checkbox: {checkbox_line:?}"
);
let regular_stripped = regular_line.trim_start();
assert!(
regular_stripped.starts_with("- "),
"bullet should remain for regular items: {regular_line:?}"
);
}
#[test]
fn test_pretty_list_and_pretty_checkbox_coexist() {
let md = "- [ ] checkbox item\n- regular item\n";
let stdout = run(&["--pretty-list", "--pretty-checkbox", "square"], md);
let checkbox_line = stdout
.lines()
.find(|l| l.contains("checkbox item"))
.unwrap();
let regular_line = stdout.lines().find(|l| l.contains("regular item")).unwrap();
assert!(
checkbox_line.contains('\u{F0131}'),
"checkbox icon missing: {checkbox_line:?}"
);
assert!(
!checkbox_line.contains('\u{F444}'),
"pretty-list bullet icon should be stripped for checkbox item: {checkbox_line:?}"
);
assert!(
regular_line.contains('\u{F444}'),
"pretty-list bullet icon should remain for regular item: {regular_line:?}"
);
}
#[test]
fn test_custom_list_marker_stripped_for_checkbox() {
let md = "- [ ] checkbox\n- regular\n";
for icon in ["*", ">>>", "* *"] {
let marker = format!("1:{icon}");
let stdout = run(
&[
"--pretty-list",
"--custom-list",
marker.as_str(),
"--pretty-checkbox",
"square",
],
md,
);
let checkbox_line = stdout.lines().find(|l| l.contains("checkbox")).unwrap();
let regular_line = stdout.lines().find(|l| l.contains("regular")).unwrap();
assert!(
!checkbox_line.contains(icon),
"custom marker {icon:?} should be stripped for checkbox: {checkbox_line:?}"
);
assert!(
regular_line.contains(icon),
"custom marker {icon:?} should remain for regular: {regular_line:?}"
);
}
}
#[test]
fn test_custom_checkbox_color_only_existing_state() {
let md = "- [?] question\n";
let stdout = run_with_colors(&["--pretty-checkbox", "square", "-B", "?:yellow"], md);
let line = stdout.lines().find(|l| l.contains("question")).unwrap();
assert!(
line.contains('\u{F078B}'),
"default icon should remain for color-only override: {line:?}"
);
assert!(
line.contains("\x1b[93m") || line.contains("\x1b[33m") || line.contains("\x1b[38;5;3m"),
"yellow color not applied for color-only override: {line:?}"
);
}
#[test]
fn test_custom_checkbox_color_only_new_state() {
let md = "- [*] starred\n";
let stdout = run_with_colors(&["--pretty-checkbox", "square", "-B", "*:yellow"], md);
let line = stdout.lines().find(|l| l.contains("starred")).unwrap();
assert!(
line.contains('\u{F0131}'),
"new state with no icon should use default unchecked icon: {line:?}"
);
assert!(
line.contains("\x1b[93m") || line.contains("\x1b[33m") || line.contains("\x1b[38;5;3m"),
"yellow color not applied for new state: {line:?}"
);
}
#[test]
fn test_custom_checkbox_icon_and_color_together() {
let md = "- [*] starred\n";
let stdout = run_with_colors(
&["--pretty-checkbox", "square", "-B", "*:\u{F078B}:red"],
md,
);
let line = stdout.lines().find(|l| l.contains("starred")).unwrap();
assert!(
line.contains('\u{F078B}'),
"custom icon should be used: {line:?}"
);
assert!(
line.contains("\x1b[31m") || line.contains("\x1b[91m") || line.contains("\x1b[38;5;1m"),
"red color not applied: {line:?}"
);
}