use super::*;
use crate::base::{Point, Size};
use crate::render::md::Heading;
use crate::theme::default_theme;
use crate::widgets::test_util::{draw_into, row};
use crate::widgets::MarkdownView;
const TABLE_DOC: &str = "| Name | N |\n|:-----|--:|\n| alpha | 1 |\n| beta | 22 |";
fn rows_plain(source: &str, width: i32) -> Vec<String> {
let t = default_theme().tokens;
layout_doc(source, &t, width)
.rows
.iter()
.map(|r| {
if r.rule {
"<rule>".to_string()
} else {
r.line.plain()
}
})
.collect()
}
#[test]
fn table_typesets_alignment_padding_and_separator() {
let rows = rows_plain(TABLE_DOC, 28);
assert_eq!(rows[0], "Name N", "left header + right-aligned header");
assert_eq!(rows[1], "─".repeat(8), "separator spans the table width");
assert_eq!(rows[2], "alpha 1", "right-aligned numeric cell");
assert_eq!(rows[3], "beta 22");
}
#[test]
fn table_center_alignment_and_cell_truncation() {
let doc = "| head |\n|:---:|\n| ab |\n| much longer content |";
let rows = rows_plain(doc, 40);
assert_eq!(rows[0], " head ");
assert_eq!(rows[2], " ab ");
let rows = rows_plain(doc, 9);
assert_eq!(rows[2], " ab ", "{rows:?}");
assert!(rows[3].ends_with('…'), "{rows:?}");
assert!(rows[3].starts_with("much"), "{rows:?}");
}
#[test]
fn table_draws_bold_header_and_border_separator() {
let t = default_theme().tokens;
let c = draw_into(MarkdownView::new(TABLE_DOC).element(&t), Size::new(28, 6));
assert!(row(&c, 0).starts_with("Name"));
assert!(
c.attrs_at(Point::new(0, 0))
.contains(crate::render::Attrs::BOLD),
"header renders bold"
);
assert_eq!(c.cell(Point::new(0, 1)).unwrap().0, '─');
assert_eq!(c.cell(Point::new(0, 1)).unwrap().1, t.border);
assert!(row(&c, 2).starts_with("alpha"));
}
#[test]
fn table_inline_spans_keep_their_styles_in_cells() {
let t = default_theme().tokens;
let doc = "| c |\n|---|\n| `code` x |";
let c = draw_into(MarkdownView::new(doc).element(&t), Size::new(20, 4));
let body = row(&c, 2);
let cx = body.find("code").unwrap() as i32;
assert_eq!(
c.cell(Point::new(cx, 2)).unwrap().2,
t.surface_raised,
"inline code chip ground survives the cell recipe"
);
}
#[test]
fn crushed_columns_floor_instead_of_zeroing() {
let doc = format!(
"| alpha | betas | {c} |\n|---|---|---|\n| aaaaa | bbbbb | {c} |",
c = "c".repeat(30)
);
let rows = rows_plain(&doc, 14);
assert!(
rows[0].contains("al…") && rows[0].contains("be…"),
"floored header cells stay identifiable: {rows:?}"
);
assert!(
rows[2].contains("aa…") && rows[2].contains("bb…"),
"floored body cells stay identifiable: {rows:?}"
);
}
#[test]
fn unfittable_table_degrades_to_records() {
let doc = format!(
"| a | b | c |\n|---|---|---|\n| one | two | {c} |\n| uno | dos | tres |",
c = "c".repeat(30)
);
let rows = rows_plain(&doc, 8);
assert!(
rows.iter().any(|r| r.starts_with("a: one")),
"record lines carry header + value: {rows:?}"
);
assert!(
rows.iter().any(|r| r.starts_with("b: two")),
"the column the old solver zeroed renders: {rows:?}"
);
assert!(
rows.iter().any(|r| r.trim_end() == "c:"),
"long values open their labeled record line: {rows:?}"
);
assert!(
rows.iter().any(|r| r.starts_with("cccccccc")),
"the long value word-wraps below its label, nothing dropped: {rows:?}"
);
assert!(
rows.iter().any(|r| r.starts_with("b: dos")),
"second record renders after a blank separator: {rows:?}"
);
let first_rec = rows.iter().position(|r| r.starts_with("a: one")).unwrap();
let second_rec = rows.iter().position(|r| r.starts_with("a: uno")).unwrap();
assert!(
(first_rec..second_rec).any(|i| rows[i].trim().is_empty()),
"records separate with a blank row: {rows:?}"
);
}
#[test]
fn numeric_columns_right_align_only_when_undeclared() {
let auto = "| item | qty |\n|------|-----|\n| ap | 10 |\n| pe | 5 |";
let rows = rows_plain(auto, 20);
assert_eq!(rows[2], "ap 10", "bare --- + numbers = right-aligned");
assert_eq!(rows[3], "pe 5", "{rows:?}");
let declared = "| item | qty |\n|------|:----|\n| ap | 10 |\n| pe | 5 |";
let rows = rows_plain(declared, 20);
assert_eq!(rows[2], "ap 10 ", "explicit :-- stays left-aligned");
let words = "| item | note |\n|------|------|\n| ap | 10 up |\n| pe | 5 |";
let rows = rows_plain(words, 20);
assert!(
rows[2].starts_with("ap 10 up"),
"a word-shaped cell keeps the column left: {rows:?}"
);
}
#[test]
fn zero_and_tiny_widths_never_panic() {
for w in [0, 1, 2, 3, 5] {
let _ = rows_plain(TABLE_DOC, w);
}
let t = default_theme().tokens;
for size in [Size::new(0, 0), Size::new(2, 1), Size::new(6, 2)] {
let _ = draw_into(MarkdownView::new(TABLE_DOC).element(&t), size);
}
}
#[test]
fn task_rows_render_checkbox_markers_in_marker_ink() {
let t = default_theme().tokens;
let doc = "- [ ] open thing\n- [x] done thing\n - [X] nested";
let c = draw_into(MarkdownView::new(doc).element(&t), Size::new(30, 4));
assert!(
row(&c, 0).starts_with(" [ ] open thing"),
"{:?}",
row(&c, 0)
);
assert!(row(&c, 1).starts_with(" [x] done thing"));
assert!(row(&c, 2).starts_with(" [x] nested"), "depth indents");
let mx = row(&c, 0).find('[').unwrap() as i32;
assert_eq!(c.cell(Point::new(mx, 0)).unwrap().1, t.accent_alt);
}
#[test]
fn task_content_wraps_with_hanging_indent() {
let doc = "- [x] a rather long task item that wraps";
let rows = rows_plain(doc, 18);
assert!(rows.len() >= 2, "{rows:?}");
assert!(rows[0].starts_with("[x] a rather"), "{rows:?}");
}
#[test]
fn outline_rows_match_the_typeset_fold_across_widths() {
let t = default_theme().tokens;
let doc = "\
# Intro
Some paragraph text that will wrap at narrow widths for sure.
## Setup steps
| a | b |
|---|---|
| 1 | 2 |
### Deep dive
- [ ] task
- item
## Setup steps
tail paragraph
";
let expected: Vec<Heading> = crate::render::md::outline(doc);
assert_eq!(expected.len(), 4);
for width in [10, 16, 24, 40, 80] {
let entries = outline_rows(doc, &t, width);
assert_eq!(entries.len(), expected.len(), "width {width}");
let fold = layout_doc(doc, &t, width);
let mut last = 0usize;
for (entry, want) in entries.iter().zip(&expected) {
assert_eq!(&entry.heading, want, "width {width}");
assert!(
entry.row >= last,
"rows monotonic at width {width}: {entries:?}"
);
last = entry.row;
let plain = fold.rows[entry.row].line.plain();
assert!(
plain.ends_with(&entry.heading.text),
"width {width}, anchor {}: {plain:?} vs {:?}",
entry.heading.anchor_id,
entry.heading.text
);
if entry.heading.level >= 3 {
assert!(
plain.starts_with(&"#".repeat(entry.heading.level as usize)),
"H3+ rows carry the level prefix: {plain:?}"
);
} else {
assert_eq!(plain, entry.heading.text);
}
}
}
}
#[test]
fn resolve_anchor_finds_rows_and_scroll_reaches_them() {
let t = default_theme().tokens;
let doc = "# Top\n\nfiller one\n\nfiller two\n\n## Target\n\nbody";
let width = 30;
let row_idx = MarkdownView::resolve_anchor(doc, &t, width, "#target").unwrap();
assert!(row_idx > 0);
assert_eq!(
MarkdownView::resolve_anchor(doc, &t, width, "target"),
Some(row_idx),
"leading # optional"
);
assert_eq!(
MarkdownView::resolve_anchor(doc, &t, width, "#missing"),
None
);
let c = draw_into(
MarkdownView::new(doc)
.scroll_offset(row_idx as i32)
.element(&t),
Size::new(width, 4),
);
assert!(row(&c, 0).starts_with("Target"), "{:?}", row(&c, 0));
}
#[test]
fn core_sources_typeset_identically_through_the_doc_fold() {
let t = default_theme().tokens;
let doc = "# T\n\npara **bold**\n\n- li\n\n> q\n\n```\ncode\n```\n\n---";
let ts = BlockTypesetter::new(&t);
let mut core_rows: Vec<Row> = Vec::new();
for block in crate::render::md::parse(doc, ts.styles()) {
ts.push_block(&mut core_rows, &block, 28, true);
}
let doc_rows = layout_doc(doc, &t, 28).rows;
assert_eq!(core_rows.len(), doc_rows.len());
for (a, b) in core_rows.iter().zip(&doc_rows) {
assert_eq!(a.line.plain(), b.line.plain());
assert_eq!(a.indent, b.indent);
assert_eq!(a.rule, b.rule);
}
}