use crossterm::style::Color;
use htui::html::{css_parse, html_parse};
use htui::layout::layout::TermMap;
use htui::tdom::node::{NodeArena, NodeId};
use htui::tui::graph_api::TermLayout;
fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}
fn setup(html: &str, css: &str) -> (NodeId, NodeArena) {
let mut html_parser = html_parse::Parser::new(html);
let (root_id, mut arena, embedded) = html_parser.parse();
let combined = if css.is_empty() {
embedded
} else if embedded.is_empty() {
css.to_string()
} else {
format!("{embedded}\n{css}")
};
if !combined.trim().is_empty() {
let mut css_parser = css_parse::Parser::new(&combined);
let rules = css_parser.parse();
css_parse::css_into_node(&mut arena, root_id, &rules);
}
(root_id, arena)
}
const COLS: u16 = 80;
const ROWS: u16 = 24;
fn mock_term_map() -> TermMap {
TermMap {
columns: COLS,
rows: ROWS,
}
}
#[test]
fn test_div_basic() {
init();
let (root_id, arena) = setup("<div>hello</div>", "");
let term_map = mock_term_map();
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let canvas: String = tl.layout.iter().map(|b| b.word.as_str()).collect();
assert!(canvas.contains("hello"), "div should render text");
}
#[test]
fn test_div_with_color() {
init();
let html = "<div>colored</div>";
let css = "div { color: blue; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let has_fg = tl
.layout
.iter()
.any(|b| b.fcolor == Some(Color::Rgb { r: 0, g: 0, b: 255 }));
assert!(has_fg, "div color should be applied to text cells");
}
#[test]
fn test_div_background_color() {
init();
let html = "<div>bg</div>";
let css = "div { background-color: yellow; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let has_bg = tl.layout.iter().any(|b| {
b.bcolor
== Some(Color::Rgb {
r: 255,
g: 255,
b: 0,
})
});
assert!(has_bg, "div background-color should be applied");
}
#[test]
fn test_div_text_and_block_child_stack_on_separate_rows() {
init();
let html = "<div>parent text<p>child block</p></div>";
let (root_id, arena) = setup(html, "");
let term_map = mock_term_map();
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let cols = term_map.columns as usize;
let rows: Vec<String> = tl
.layout
.chunks(cols)
.map(|chunk| chunk.iter().map(|b| b.word.as_str()).collect())
.collect();
let parent_row = rows
.iter()
.position(|r| r.contains("parent text"))
.expect("parent text should render");
let child_row = rows
.iter()
.position(|r| r.contains("child block"))
.expect("child block should render");
assert!(
parent_row < child_row,
"parent text must sit above the block child, got {parent_row}/{child_row}"
);
}
#[test]
fn test_div_bg_fills_full_row_default() {
init();
let html = "<div>short</div>";
let css = "div { background-color: red; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let content_idx = tl
.layout
.iter()
.position(|b| b.word != " " && b.bcolor == Some(Color::Rgb { r: 255, g: 0, b: 0 }))
.expect("should find colored content");
let row = content_idx / cols;
let rstart = row * cols;
let rend = rstart + cols;
let bg_count = tl.layout[rstart..rend]
.iter()
.filter(|b| b.bcolor == Some(Color::Rgb { r: 255, g: 0, b: 0 }))
.count();
assert_eq!(bg_count, cols, "default width should fill entire row");
}
#[test]
fn test_div_width_50pct_left() {
init();
let html = "<div>half</div>";
let css = "div { width: 50%; background-color: blue; color: white; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let content_idx = tl
.layout
.iter()
.position(|b| b.bcolor.is_some() && b.word != " ")
.expect("should find colored content");
let content_row = content_idx / cols;
let row = &tl.layout[content_row * cols..(content_row + 1) * cols];
let block_start = 0;
let block_end = cols / 2;
let mut has_text = false;
for (i, cell) in row.iter().enumerate().take(block_end).skip(block_start) {
assert!(
cell.bcolor.is_some(),
"block should have bg col={i} (flush left)"
);
if cell.word != " " {
assert_eq!(
cell.fcolor,
Some(Color::Rgb {
r: 255,
g: 255,
b: 255
}),
"text should be white"
);
has_text = true;
}
}
assert!(has_text, "block should contain text");
for cell in row.iter().take(cols).skip(block_end) {
assert!(
cell.bcolor.is_none(),
"cols cols/2..cols should be bg-free (outside the block)"
);
}
}
#[test]
fn test_div_width_fixed() {
init();
let html = "<div>Fix</div>";
let css = "div { width: 20ch; background-color: red; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let content_idx = tl
.layout
.iter()
.position(|b| b.bcolor.is_some() && b.word != " ")
.expect("should find content");
let row = content_idx / cols;
let rstart = row * cols;
let rend = rstart + cols;
let bg_count = (rstart..rend)
.filter(|&i| tl.layout[i].bcolor.is_some())
.count();
assert_eq!(
bg_count, 20,
"background should be exactly 20 columns (cols 0..20, flush left)"
);
let first_bg_col = (rstart..rend)
.find(|&i| tl.layout[i].bcolor.is_some())
.map(|i| i - rstart)
.expect("block should have at least one bg cell");
assert_eq!(
first_bg_col, 0,
"fixed-width block must sit flush left at col 0"
);
}
#[test]
fn test_div_text_align_center() {
init();
let html = "<div>Centered</div>";
let css = "div { text-align: center; width: 40ch; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let text_idx = tl
.layout
.iter()
.position(|b| b.word == *"C")
.expect("should find text");
let col = text_idx % cols;
let expected_padding = (40 - "Centered".len()) / 2;
assert_eq!(
col, expected_padding,
"text should be centered within the (flush-left) 40-col block"
);
}
#[test]
fn test_div_text_align_right() {
init();
let html = "<div>Right</div>";
let css = "div { text-align: right; width: 40ch; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let text_idx = tl
.layout
.iter()
.position(|b| b.word == *"R")
.expect("should find text");
let col = text_idx % cols;
let expected_padding = 40 - "Right".len();
assert_eq!(
col, expected_padding,
"text should be right-aligned within the (flush-left) 40-col block"
);
}
#[test]
fn test_div_nested_preserves_text() {
init();
let html = "<div><p>A</p><div><p>B</p></div></div>";
let (root_id, arena) = setup(html, "");
let term_map = mock_term_map();
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let canvas: String = tl.layout.iter().map(|b| b.word.as_str()).collect();
assert!(canvas.contains("A"), "outer div should contain A");
assert!(canvas.contains("B"), "inner div should contain B");
}
#[test]
fn test_div_no_bg_does_not_fill_row() {
init();
let html = "<div>hi</div>";
let (root_id, arena) = setup(html, "");
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let text_idx = tl.layout.iter().position(|b| b.word != " ").unwrap();
let row = text_idx / cols;
let rstart = row * cols;
let rend = rstart + cols;
let has_bg = tl.layout[rstart..rend].iter().any(|b| b.bcolor.is_some());
assert!(
!has_bg,
"row should not have any bg cells when div has no background-color"
);
}
#[test]
fn test_div_color_inherited_by_p() {
init();
let html = "<div><p>colored</p></div>";
let css = "div { color: magenta; }";
let (root_id, arena) = setup(html, css);
let term_map = mock_term_map();
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let has_color = tl.layout.iter().any(|b| {
b.fcolor
== Some(Color::Rgb {
r: 255,
g: 0,
b: 255,
})
});
assert!(has_color, "div color should inherit to <p> descendant");
}
#[test]
fn test_long_p_text_does_not_overflow_into_next_row() {
init();
let long_text = "A longer paragraph that contains multiple sentences. This demonstrates how text wrapping and layout work in the terminal renderer. The text should flow naturally within the available terminal width.";
let html = format!("<p>{}</p><h2>Next</h2>", long_text);
let (root_id, arena) = setup(&html, "");
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let p_row = 0;
let p_row_end = (p_row + 1) * cols;
for cell in &tl.layout[p_row_end..] {
let has_p_text = !cell.word.is_empty()
&& cell.word != " "
&& cell.fcolor
== Some(Color::Rgb {
r: 128,
g: 128,
b: 128,
});
assert!(
!has_p_text,
"p text should not bleed past row 0 — found {:?} at cell index {}",
cell.word, p_row_end
);
}
}
#[test]
fn test_long_p_text_stops_at_content_box_right_edge() {
init();
let long_text = "x".repeat(200);
let html = format!("<p>{}</p>", long_text);
let (root_id, arena) = setup(&html, "");
let term_map = mock_term_map();
let cols = term_map.columns as usize;
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
let row1_start = cols;
let row1_end = 2 * cols;
let p_chars_on_row1 = tl.layout[row1_start..row1_end]
.iter()
.filter(|c| {
c.fcolor
== Some(Color::Rgb {
r: 128,
g: 128,
b: 128,
})
&& c.word == "x"
})
.count();
assert_eq!(
p_chars_on_row1, 0,
"no 'x' characters should spill from the p onto row 1"
);
}