use super::*;
use crate::prelude::*;
fn pipeline(dom: &mut TuiDom, sheet: &Stylesheet, viewport: Rect) -> Buffer {
dom.cascade(sheet);
dom.layout_dom(viewport);
let mut buf = Buffer::empty(viewport);
dom.paint_dom(&mut buf, viewport);
buf
}
fn row(buf: &Buffer, y: u16) -> String {
let mut s = String::new();
for x in buf.area.x..buf.area.right() {
if let Some(c) = buf.cell(x, y) {
if c.is_spacer() {
continue;
}
s.push_str(c.symbol());
}
}
s
}
#[cfg(test)]
fn treeitem(dom: &mut TuiDom, label: &str, attrs: &[(&str, &str)]) -> rdom_core::NodeId {
let li = dom.create_element("li");
dom.set_attribute(li, "role", "treeitem").unwrap();
for (k, v) in attrs {
dom.set_attribute(li, k, v).unwrap();
}
let t = dom.create_text_node(label);
dom.append_child(li, t).unwrap();
li
}
#[test]
fn tree_guides_draw_connectors_and_trunks() {
let mut dom = TuiDom::new();
let root = dom.root();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(root, tree).unwrap();
let cluster = treeitem(&mut dom, "Cluster", &[("aria-expanded", "true")]);
dom.append_child(tree, cluster).unwrap();
let cgroup = dom.create_element("ul");
dom.set_attribute(cgroup, "role", "group").unwrap();
dom.append_child(cluster, cgroup).unwrap();
let pods = treeitem(&mut dom, "Pods", &[]);
let services = treeitem(&mut dom, "Services", &[]);
let nodes = treeitem(&mut dom, "Nodes", &[("aria-expanded", "true")]);
dom.append_child(cgroup, pods).unwrap();
dom.append_child(cgroup, services).unwrap();
dom.append_child(cgroup, nodes).unwrap();
let ngroup = dom.create_element("ul");
dom.set_attribute(ngroup, "role", "group").unwrap();
dom.append_child(nodes, ngroup).unwrap();
let n1 = treeitem(&mut dom, "node-1", &[]);
let n2 = treeitem(&mut dom, "node-2", &[]);
dom.append_child(ngroup, n1).unwrap();
dom.append_child(ngroup, n2).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 24, 6));
let rows: Vec<String> = (0..6)
.map(|y| row(&buf, y).trim_end().to_string())
.collect();
assert_eq!(
rows,
vec![
"▼ Cluster",
"├ Pods",
"├ Services",
"└ ▼ Nodes",
" ├ node-1",
" └ node-2",
],
"actual tree render:\n{}",
rows.join("\n"),
);
}
#[test]
fn tree_guides_clip_to_an_overflow_hidden_ancestor() {
let mut dom = TuiDom::new();
let root = dom.root();
let wrap = dom.create_element("div");
dom.set_attribute(wrap, "class", "wrap").unwrap();
dom.append_child(root, wrap).unwrap();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(wrap, tree).unwrap();
let branch = treeitem(&mut dom, "Branch", &[("aria-expanded", "true")]);
dom.append_child(tree, branch).unwrap();
let group = dom.create_element("ul");
dom.set_attribute(group, "role", "group").unwrap();
dom.append_child(branch, group).unwrap();
for name in ["one", "two", "three", "four", "five"] {
let li = treeitem(&mut dom, name, &[]);
dom.append_child(group, li).unwrap();
}
let sheet = rdom_css::from_css(".wrap { height: 3; overflow: hidden; }");
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 24, 8));
for y in 3..8 {
let line = row(&buf, y);
assert!(
!line.contains('│') && !line.contains('├') && !line.contains('└'),
"guide glyph leaked below the overflow:hidden clip at row {y}: {line:?}"
);
}
}
#[test]
fn mixed_content_block_leading_text_clips_instead_of_clamping_when_scrolled_above() {
let mut dom = TuiDom::new();
let root = dom.root();
let wrap = dom.create_element("div");
dom.set_attribute(wrap, "class", "wrap").unwrap();
dom.append_child(root, wrap).unwrap();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(wrap, tree).unwrap();
let branch = treeitem(&mut dom, "Category", &[("aria-expanded", "true")]);
dom.append_child(tree, branch).unwrap();
let group = dom.create_element("ul");
dom.set_attribute(group, "role", "group").unwrap();
dom.append_child(branch, group).unwrap();
let leaf = treeitem(&mut dom, "kitten", &[]);
dom.append_child(group, leaf).unwrap();
for name in ["puppy", "fish", "bird"] {
let l = treeitem(&mut dom, name, &[]);
dom.append_child(group, l).unwrap();
}
let sheet = rdom_css::from_css(".wrap { height: 3; overflow: hidden; }");
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 16, 4));
if let Some(ext) = dom.node_mut(wrap).ext_mut() {
ext.scroll_y = 1;
}
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 16, 4));
let mut buf = Buffer::empty(Rect::new(0, 0, 16, 4));
dom.paint_dom(&mut buf, Rect::new(0, 0, 16, 4));
let row0 = row(&buf, 0);
let labels: String = row0
.chars()
.filter(|c| c.is_ascii_alphabetic() || *c == ' ')
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert_eq!(
labels, "kitten",
"scrolled-off 'Category' label must not clamp onto the first visible row \
(would read like 'Categ kitten'). Raw row: {row0:?}"
);
}
#[test]
fn tree_guides_continue_through_a_wrapped_label() {
let mut dom = TuiDom::new();
let root = dom.root();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(root, tree).unwrap();
let branch = treeitem(&mut dom, "Cat", &[("aria-expanded", "true")]);
dom.append_child(tree, branch).unwrap();
let group = dom.create_element("ul");
dom.set_attribute(group, "role", "group").unwrap();
dom.append_child(branch, group).unwrap();
let long = treeitem(&mut dom, "alpha beta gamma", &[]);
let short = treeitem(&mut dom, "z", &[]);
dom.append_child(group, long).unwrap();
dom.append_child(group, short).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 12, 8));
let lrect = dom.node(long).layout_rect().unwrap();
assert!(
lrect.height >= 2,
"label should wrap to >=2 rows at width 12 (got {})",
lrect.height
);
let own_col = (lrect.x - 2) as u16;
for dy in 1..lrect.height {
let y = (lrect.y + dy as i32) as u16;
let sym = buf
.cell(own_col, y)
.map(|c| c.symbol().to_string())
.unwrap_or_default();
assert!(
sym == "│" || sym == "├",
"trunk broke at wrapped row y={y}, col={own_col}: expected vertical guide, got {sym:?}"
);
}
}
#[test]
fn tree_guides_continue_trunk_past_expanded_nonlast_branch() {
let mut dom = TuiDom::new();
let root = dom.root();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(root, tree).unwrap();
let a = treeitem(&mut dom, "A", &[("aria-expanded", "true")]);
dom.append_child(tree, a).unwrap();
let ag = dom.create_element("ul");
dom.set_attribute(ag, "role", "group").unwrap();
dom.append_child(a, ag).unwrap();
let b = treeitem(&mut dom, "B", &[("aria-expanded", "true")]); dom.append_child(ag, b).unwrap();
let bg = dom.create_element("ul");
dom.set_attribute(bg, "role", "group").unwrap();
dom.append_child(b, bg).unwrap();
let b1 = treeitem(&mut dom, "B1", &[]);
dom.append_child(bg, b1).unwrap();
let c = treeitem(&mut dom, "C", &[]); dom.append_child(ag, c).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 4));
let rows: Vec<String> = (0..4)
.map(|y| row(&buf, y).trim_end().to_string())
.collect();
assert_eq!(
rows,
vec!["▼ A", "├ ▼ B", "│ └ B1", "└ C"],
"actual:\n{}",
rows.join("\n"),
);
}
#[test]
fn tree_collapsed_branch_hides_children_and_shows_collapsed_chevron() {
let mut dom = TuiDom::new();
let root = dom.root();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(root, tree).unwrap();
let a = treeitem(&mut dom, "A", &[("aria-expanded", "false")]);
dom.append_child(tree, a).unwrap();
let ag = dom.create_element("ul");
dom.set_attribute(ag, "role", "group").unwrap();
dom.append_child(a, ag).unwrap();
let hidden = treeitem(&mut dom, "hidden-child", &[]);
dom.append_child(ag, hidden).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 4));
assert_eq!(row(&buf, 0).trim_end(), "▶ A", "collapsed arrow");
assert_eq!(row(&buf, 1).trim_end(), "", "child group must be hidden");
}
#[test]
fn tree_active_row_highlight_fills_the_trees_own_padding() {
let mut dom = TuiDom::new();
let root = dom.root();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.set_attribute(tree, "class", "t").unwrap();
dom.append_child(root, tree).unwrap();
let x = treeitem(&mut dom, "X", &[("data-rdom-active", "")]);
dom.append_child(tree, x).unwrap();
dom.set_focused(Some(tree));
let sheet = rdom_css::from_css(".t { padding-left: 2; }");
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 12, 2));
let hl = Color::Rgb(0x2d, 0x2f, 0x31);
assert_eq!(
buf.cell(0, 0).unwrap().bg,
hl,
"highlight must fill the tree's left padding (col 0), not stop at the content box"
);
assert_eq!(
buf.cell(1, 0).unwrap().bg,
hl,
"and the rest of the padding"
);
}
#[test]
fn tree_active_row_highlight_spans_full_width_under_guides() {
let mut dom = TuiDom::new();
let root = dom.root();
let tree = dom.create_element("ul");
dom.set_attribute(tree, "role", "tree").unwrap();
dom.append_child(root, tree).unwrap();
let a = treeitem(&mut dom, "A", &[("aria-expanded", "true")]);
dom.append_child(tree, a).unwrap();
let ag = dom.create_element("ul");
dom.set_attribute(ag, "role", "group").unwrap();
dom.append_child(a, ag).unwrap();
let child = treeitem(&mut dom, "child", &[("data-rdom-active", "")]);
dom.append_child(ag, child).unwrap();
dom.set_focused(Some(tree));
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 3));
let hl = Color::Rgb(0x2d, 0x2f, 0x31);
assert_eq!(
buf.cell(0, 1).unwrap().bg,
hl,
"highlight fills the guide gutter (col 0), not just the box"
);
assert_eq!(
buf.cell(0, 1).unwrap().symbol(),
"└",
"the guide glyph still renders on the highlighted gutter"
);
assert_eq!(buf.cell(10, 1).unwrap().bg, hl, "and the rest of the row");
assert_ne!(buf.cell(0, 0).unwrap().bg, hl, "non-cursor row not filled");
}
#[test]
fn single_text_span_paints() {
let mut dom = TuiDom::new();
let root = dom.root();
let span = dom.create_element("span");
let t = dom.create_text_node("hello");
dom.append_child(span, t).unwrap();
dom.append_child(root, span).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "hello");
}
#[test]
fn cjk_text_paints_with_spacer() {
let mut dom = TuiDom::new();
let root = dom.root();
let span = dom.create_element("span");
let t = dom.create_text_node("中");
dom.append_child(span, t).unwrap();
dom.append_child(root, span).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 4, 1));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "中");
assert!(buf.cell(1, 0).unwrap().is_spacer());
}
#[test]
fn emoji_paints() {
let mut dom = TuiDom::new();
let root = dom.root();
let span = dom.create_element("span");
let t = dom.create_text_node("🦀 rust");
dom.append_child(span, t).unwrap();
dom.append_child(root, span).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 10, 1));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "🦀");
assert!(buf.cell(1, 0).unwrap().is_spacer());
assert_eq!(buf.cell(2, 0).unwrap().symbol(), " ");
assert_eq!(buf.cell(3, 0).unwrap().symbol(), "r");
}
#[test]
fn bg_fills_outer_rect_css_way() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(3))
.bg(Color::Rgb(255, 0, 0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
for y in 0..3 {
for x in 0..5 {
assert_eq!(buf.cell(x, y).unwrap().bg, Color::Rgb(255, 0, 0));
}
}
assert_eq!(buf.cell(6, 0).unwrap().bg, Color::Reset);
}
#[test]
fn bg_covers_border_cells() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(3))
.bg(Color::Rgb(255, 0, 0))
.border(Border::single())
.border_fg(Color::Rgb(255, 255, 255)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
let tl = buf.cell(0, 0).unwrap();
assert_eq!(tl.symbol(), "┌");
assert_eq!(tl.fg, Color::Rgb(255, 255, 255));
assert_eq!(tl.bg, Color::Rgb(255, 0, 0));
}
#[test]
fn border_single_all_four_sides() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(4))
.height(Size::Fixed(3))
.border(Border::single()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "┌");
assert_eq!(buf.cell(3, 0).unwrap().symbol(), "┐");
assert_eq!(buf.cell(0, 2).unwrap().symbol(), "└");
assert_eq!(buf.cell(3, 2).unwrap().symbol(), "┘");
assert_eq!(buf.cell(1, 0).unwrap().symbol(), "─");
assert_eq!(buf.cell(0, 1).unwrap().symbol(), "│");
assert_eq!(buf.cell(3, 1).unwrap().symbol(), "│");
}
#[test]
fn border_rounded_uses_curves() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(4))
.height(Size::Fixed(3))
.border(Border::rounded()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "╭");
assert_eq!(buf.cell(3, 0).unwrap().symbol(), "╮");
assert_eq!(buf.cell(0, 2).unwrap().symbol(), "╰");
assert_eq!(buf.cell(3, 2).unwrap().symbol(), "╯");
}
#[test]
fn border_half_block_uses_block_glyphs() {
use crate::layout::BorderStyle;
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(4))
.height(Size::Fixed(3))
.border(Border::ring(BorderStyle::HalfBlock)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "▗", "top-left");
assert_eq!(buf.cell(3, 0).unwrap().symbol(), "▖", "top-right");
assert_eq!(buf.cell(0, 2).unwrap().symbol(), "▝", "bottom-left");
assert_eq!(buf.cell(3, 2).unwrap().symbol(), "▘", "bottom-right");
assert_eq!(buf.cell(1, 0).unwrap().symbol(), "▄", "top edge");
assert_eq!(buf.cell(2, 0).unwrap().symbol(), "▄", "top edge");
assert_eq!(buf.cell(1, 2).unwrap().symbol(), "▀", "bottom edge");
assert_eq!(buf.cell(2, 2).unwrap().symbol(), "▀", "bottom edge");
assert_eq!(buf.cell(0, 1).unwrap().symbol(), "▐", "left edge");
assert_eq!(buf.cell(3, 1).unwrap().symbol(), "▌", "right edge");
}
#[test]
fn border_top_only() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(4))
.height(Size::Fixed(3))
.border(Border::top()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "─");
assert_eq!(buf.cell(3, 0).unwrap().symbol(), "─");
assert_eq!(buf.cell(0, 1).unwrap().symbol(), " ");
assert_eq!(buf.cell(0, 2).unwrap().symbol(), " ");
}
#[test]
fn border_fg_applies() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(3))
.height(Size::Fixed(2))
.border(Border::single())
.border_fg(Color::Rgb(0, 255, 255)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().fg, Color::Rgb(0, 255, 255));
}
#[test]
fn border_with_no_bg_preserves_parent_bg() {
let mut dom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("p");
let child = dom.create_element("c");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.bg(Color::Rgb(50, 50, 50)),
)
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::rounded())
.border_fg(Color::Rgb(200, 200, 200)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 12, 6));
let tl = buf.cell(0, 0).unwrap();
assert_eq!(tl.symbol(), "╭");
assert_eq!(tl.fg, Color::Rgb(200, 200, 200));
assert_eq!(
tl.bg,
Color::Rgb(50, 50, 50),
"border ring must preserve parent bg when child has no background-color"
);
let top_horizontal = buf.cell(3, 0).unwrap();
assert_eq!(top_horizontal.symbol(), "─");
assert_eq!(top_horizontal.bg, Color::Rgb(50, 50, 50));
}
#[test]
fn text_inside_bordered_box_respects_inset() {
let mut dom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("d");
let t = dom.create_text_node("hi");
dom.append_child(d, t).unwrap();
dom.append_child(root, d).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"d",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::single()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "┌");
assert_eq!(buf.cell(1, 1).unwrap().symbol(), "h");
assert_eq!(buf.cell(2, 1).unwrap().symbol(), "i");
}
#[test]
fn before_content_paints_at_start() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("item");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"s::before",
TuiStyle::new().content(Content::Str("▾ ".into())),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), "▾ item");
}
#[test]
fn after_content_paints_after_text() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("x");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"s::after",
TuiStyle::new().content(Content::Str("!".into())),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "x!");
}
#[test]
fn before_and_after_both_paint() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("mid");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"s::before",
TuiStyle::new().content(Content::Str("[".into())),
)
.rule_unchecked(
"s::after",
TuiStyle::new().content(Content::Str("]".into())),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "[mid]");
}
#[test]
fn text_clips_at_buffer_right_edge() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("hello world");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 5, 1));
assert_eq!(row(&buf, 0), "hello");
}
#[test]
fn negative_layout_rect_partially_visible() {
let mut dom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
let t = dom.create_text_node("AAA");
dom.append_child(a, t).unwrap();
let t = dom.create_text_node("BBB");
dom.append_child(b, t).unwrap();
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(1)));
dom.cascade(&sheet);
dom.node_mut(c).ext_mut().unwrap().scroll_y = 1;
dom.layout_dom(Rect::new(0, 0, 10, 2));
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
dom.paint_dom(&mut buf, Rect::new(0, 0, 10, 2));
assert_eq!(row(&buf, 0).trim_end(), "BBB");
}
#[test]
fn nested_elements_paint_recursively() {
let mut dom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("outer");
let inner = dom.create_element("inner");
let t = dom.create_text_node("nested");
dom.append_child(inner, t).unwrap();
dom.append_child(outer, inner).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"outer",
TuiStyle::new()
.padding(Padding::all(1))
.border(Border::single())
.width(Size::Fixed(12))
.height(Size::Fixed(4)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "┌");
assert_eq!(buf.cell(11, 0).unwrap().symbol(), "┐");
assert_eq!(buf.cell(2, 2).unwrap().symbol(), "n");
assert_eq!(buf.cell(7, 2).unwrap().symbol(), "d");
}
#[test]
fn row_children_paint_at_their_layout_positions() {
let mut dom = TuiDom::new();
let root = dom.root();
let r = dom.create_element("r");
let a = dom.create_element("a");
let b = dom.create_element("b");
let t = dom.create_text_node("LEFT");
dom.append_child(a, t).unwrap();
let t = dom.create_text_node("RIGHT");
dom.append_child(b, t).unwrap();
dom.append_child(r, a).unwrap();
dom.append_child(r, b).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"r",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.gap(1),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(4)))
.rule_unchecked("b", TuiStyle::new().width(Size::Fixed(5)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), "LEFT RIGHT");
}
#[test]
fn fg_applies_to_painted_text() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("red");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("s", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(buf.cell(0, 0).unwrap().fg, Color::Rgb(255, 0, 0));
assert_eq!(buf.cell(2, 0).unwrap().fg, Color::Rgb(255, 0, 0));
}
#[test]
fn bold_modifier_applies() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("BOLD");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("s", TuiStyle::new().bold(true));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert!(buf.cell(0, 0).unwrap().modifier.contains(Modifier::BOLD));
}
#[test]
fn pseudo_element_has_its_own_fg() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("body");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("s", TuiStyle::new().fg(Color::Rgb(0, 0, 255)))
.rule_unchecked(
"s::before",
TuiStyle::new()
.content(Content::Str("▾".into()))
.fg(Color::Rgb(255, 0, 0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "▾");
assert_eq!(buf.cell(0, 0).unwrap().fg, Color::Rgb(255, 0, 0));
assert_eq!(buf.cell(1, 0).unwrap().symbol(), "b");
assert_eq!(buf.cell(1, 0).unwrap().fg, Color::Rgb(0, 0, 255));
}
#[test]
fn overflow_hidden_clips_child_text() {
let mut dom = TuiDom::new();
let root = dom.root();
let box_ = dom.create_element("box");
let span = dom.create_element("span");
let t = dom.create_text_node("overflowing text");
dom.append_child(span, t).unwrap();
dom.append_child(box_, span).unwrap();
dom.append_child(root, box_).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"box",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(1))
.overflow(Overflow::Hidden),
)
.rule_unchecked("span", TuiStyle::new().width(Size::Fixed(100)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 1));
let visible = row(&buf, 0);
assert_eq!(visible, "overfl ");
}
#[test]
fn empty_dom_paints_empty_buffer() {
let mut dom = TuiDom::new();
let sheet = Stylesheet::bare();
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 5, 2));
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 2));
dom.paint_dom(&mut buf, Rect::new(0, 0, 5, 2));
assert_eq!(row(&buf, 0), " ");
assert_eq!(row(&buf, 1), " ");
}
#[test]
fn layout_rect_to_grid_basic() {
let layout = LayoutRect::new(2, 3, 10, 5);
let clip = Rect::new(0, 0, 20, 10);
let out = layout_rect_to_grid(layout, clip).unwrap();
assert_eq!(out, Rect::new(2, 3, 10, 5));
}
#[test]
fn layout_rect_to_grid_partial_left() {
let layout = LayoutRect::new(-3, 0, 10, 5); let clip = Rect::new(0, 0, 20, 10);
let out = layout_rect_to_grid(layout, clip).unwrap();
assert_eq!(out, Rect::new(0, 0, 7, 5));
}
#[test]
fn layout_rect_to_grid_fully_negative() {
let layout = LayoutRect::new(-10, 0, 5, 5);
let clip = Rect::new(0, 0, 20, 10);
assert!(layout_rect_to_grid(layout, clip).is_none());
}
#[test]
fn layout_rect_to_grid_beyond_clip() {
let layout = LayoutRect::new(5, 5, 100, 100);
let clip = Rect::new(0, 0, 20, 10);
let out = layout_rect_to_grid(layout, clip).unwrap();
assert_eq!(out, Rect::new(5, 5, 15, 5));
}
#[test]
fn pseudo_content_with_var_resolves() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare()
.define_var("arrow", "▸")
.rule_unchecked("s", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked(
"s::before",
TuiStyle::new().content(Content::Var("arrow".into())),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "▸");
}
#[test]
fn text_inherits_element_bg_via_paint_style() {
let mut dom = TuiDom::new();
let root = dom.root();
let s = dom.create_element("s");
let t = dom.create_text_node("AB");
dom.append_child(s, t).unwrap();
dom.append_child(root, s).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"s",
TuiStyle::new()
.bg(Color::Rgb(0, 0, 0))
.fg(Color::Rgb(255, 255, 255))
.width(Size::Fixed(5)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
let a = buf.cell(0, 0).unwrap();
assert_eq!(a.symbol(), "A");
assert_eq!(a.fg, Color::Rgb(255, 255, 255));
assert_eq!(a.bg, Color::Rgb(0, 0, 0));
let c2 = buf.cell(2, 0).unwrap();
assert_eq!(c2.bg, Color::Rgb(0, 0, 0));
}
fn ifc_paragraph(text: &str) -> (TuiDom, rdom_core::NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node(text);
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
(dom, t)
}
fn ifc_sheet(width: u16) -> Stylesheet {
Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(width)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline))
}
fn ifc_sheet_with_ua_selection(width: u16) -> Stylesheet {
ifc_sheet(width).rule_unchecked(
"*::selection",
TuiStyle::new()
.bg(Color::Rgb(0x39, 0x4B, 0x7E))
.fg(Color::Rgb(0xFF, 0xFF, 0xFF)),
)
}
#[test]
fn selection_range_paints_ua_overlay_on_selected_cells_only() {
use rdom_core::{Position, Selection};
let (mut dom, t) = ifc_paragraph("hello");
dom.set_selection(Some(Selection::new(
Position::new(t, 1),
Position::new(t, 4),
)));
let buf = pipeline(
&mut dom,
&ifc_sheet_with_ua_selection(20),
Rect::new(0, 0, 20, 2),
);
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
let ua_sel_fg = Color::Rgb(0xFF, 0xFF, 0xFF);
assert_ne!(buf.cell(0, 0).unwrap().bg, ua_sel_bg);
assert_ne!(buf.cell(4, 0).unwrap().bg, ua_sel_bg);
for x in 1..=3 {
let c = buf.cell(x, 0).unwrap();
assert_eq!(c.bg, ua_sel_bg, "cell {x} bg");
assert_eq!(c.fg, ua_sel_fg, "cell {x} fg");
}
}
#[test]
fn selection_uses_author_styled_overlay_when_present() {
use rdom_core::{Position, Selection};
let (mut dom, t) = ifc_paragraph("hello");
dom.set_selection(Some(Selection::new(
Position::new(t, 1),
Position::new(t, 4),
)));
let sheet = ifc_sheet(20).rule_unchecked(
"p::selection",
TuiStyle::new()
.bg(Color::Rgb(255, 255, 0))
.fg(Color::Rgb(0, 0, 0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 2));
assert_ne!(buf.cell(0, 0).unwrap().bg, Color::Rgb(255, 255, 0));
for x in 1..=3 {
let c = buf.cell(x, 0).unwrap();
assert_eq!(c.bg, Color::Rgb(255, 255, 0), "cell {x} bg");
assert_eq!(c.fg, Color::Rgb(0, 0, 0), "cell {x} fg");
}
}
#[test]
fn selection_uses_ua_default_when_no_author_rule() {
use rdom_core::{Position, Selection};
let (mut dom, t) = ifc_paragraph("hello");
dom.set_selection(Some(Selection::new(
Position::new(t, 1),
Position::new(t, 4),
)));
let buf = pipeline(
&mut dom,
&ifc_sheet_with_ua_selection(20),
Rect::new(0, 0, 20, 2),
);
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
let ua_sel_fg = Color::Rgb(0xFF, 0xFF, 0xFF);
for x in 1..=3 {
let c = buf.cell(x, 0).unwrap();
assert_eq!(c.bg, ua_sel_bg, "cell {x} bg should be UA selection bg");
assert_eq!(c.fg, ua_sel_fg, "cell {x} fg should be UA selection fg");
}
}
#[test]
fn collapsed_selection_paints_no_selection_overlay() {
use rdom_core::{Position, Selection};
let (mut dom, t) = ifc_paragraph("hello");
dom.set_selection(Some(Selection::caret(Position::new(t, 2))));
let buf = pipeline(
&mut dom,
&ifc_sheet_with_ua_selection(20),
Rect::new(0, 0, 20, 2),
);
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
for x in 0..5 {
assert_ne!(
buf.cell(x, 0).unwrap().bg,
ua_sel_bg,
"cell {x} should not get the selection overlay for a collapsed caret"
);
}
}
#[test]
fn no_selection_paints_no_selection_overlay() {
let (mut dom, _t) = ifc_paragraph("hello");
let buf = pipeline(
&mut dom,
&ifc_sheet_with_ua_selection(20),
Rect::new(0, 0, 20, 2),
);
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
for x in 0..5 {
assert_ne!(buf.cell(x, 0).unwrap().bg, ua_sel_bg);
}
}
#[test]
fn cjk_selection_overlays_full_grapheme_cells() {
use rdom_core::{Position, Selection};
let (mut dom, t) = ifc_paragraph("中文");
dom.set_selection(Some(Selection::new(
Position::new(t, 0),
Position::new(t, 3),
)));
let buf = pipeline(
&mut dom,
&ifc_sheet_with_ua_selection(20),
Rect::new(0, 0, 20, 2),
);
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
assert_eq!(buf.cell(0, 0).unwrap().bg, ua_sel_bg);
assert_eq!(buf.cell(1, 0).unwrap().bg, ua_sel_bg);
assert_ne!(buf.cell(2, 0).unwrap().bg, ua_sel_bg);
assert_ne!(buf.cell(3, 0).unwrap().bg, ua_sel_bg);
}
#[test]
fn selection_across_inline_element_highlights_both_fragments() {
use rdom_core::{Position, Selection};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t_ab = dom.create_text_node("ab");
dom.append_child(p, t_ab).unwrap();
let code = dom.create_element("code");
let t_xy = dom.create_text_node("XY");
dom.append_child(code, t_xy).unwrap();
dom.append_child(p, code).unwrap();
let t_cd = dom.create_text_node("cd");
dom.append_child(p, t_cd).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(20)),
)
.rule_unchecked("code", TuiStyle::new().display(Display::Inline))
.rule_unchecked(
"*::selection",
TuiStyle::new()
.bg(Color::Rgb(0x39, 0x4B, 0x7E))
.fg(Color::Rgb(0xFF, 0xFF, 0xFF)),
);
dom.set_selection(Some(Selection::new(
Position::new(t_ab, 1),
Position::new(t_cd, 1),
)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 2));
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
assert_ne!(buf.cell(0, 0).unwrap().bg, ua_sel_bg); assert_eq!(buf.cell(1, 0).unwrap().bg, ua_sel_bg); assert_eq!(buf.cell(2, 0).unwrap().bg, ua_sel_bg); assert_eq!(buf.cell(3, 0).unwrap().bg, ua_sel_bg); assert_eq!(buf.cell(4, 0).unwrap().bg, ua_sel_bg); assert_ne!(buf.cell(5, 0).unwrap().bg, ua_sel_bg); }
#[test]
fn selection_on_wrapped_second_line_paints_that_row() {
use rdom_core::{Position, Selection};
let (mut dom, t) = ifc_paragraph("hello world");
dom.set_selection(Some(Selection::new(
Position::new(t, 6),
Position::new(t, 11),
)));
let buf = pipeline(
&mut dom,
&ifc_sheet_with_ua_selection(6),
Rect::new(0, 0, 10, 3),
);
let ua_sel_bg = Color::Rgb(0x39, 0x4B, 0x7E);
for x in 0..5 {
assert_ne!(
buf.cell(x, 0).unwrap().bg,
ua_sel_bg,
"row 0 cell {x} should not have the selection overlay"
);
}
for x in 0..5 {
assert_eq!(
buf.cell(x, 1).unwrap().bg,
ua_sel_bg,
"row 1 cell {x} should have the selection overlay"
);
}
}
fn paint_with_scroll_content(
overflow_x: Option<Overflow>,
overflow_y: Option<Overflow>,
content_w: usize,
content_h: usize,
) -> Buffer {
use crate::style::TuiStyle;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
let inner = dom.create_element("inner");
dom.append_child(root, c).unwrap();
dom.append_child(c, inner).unwrap();
let mut rule = TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(6));
if let Some(o) = overflow_y {
rule = rule.overflow_y(o);
}
if let Some(o) = overflow_x {
rule = rule.overflow_x(o);
}
let inner_rule = TuiStyle::new()
.width(Size::Fixed(content_w as u16))
.height(Size::Fixed(content_h as u16));
let sheet = Stylesheet::bare()
.rule_unchecked("c", rule)
.rule_unchecked("inner", inner_rule);
pipeline(&mut dom, &sheet, Rect::new(0, 0, 12, 8))
}
#[test]
fn overflow_scroll_y_always_paints_vertical_scrollbar_even_when_content_fits() {
let buf = paint_with_scroll_content(None, Some(Overflow::Scroll), 5, 5);
let col: Vec<(String, crate::style::Color)> = (0..6)
.map(|y| {
let c = buf.cell(9, y).unwrap();
(c.symbol().to_string(), c.bg)
})
.collect();
for (y, (sym, bg)) in col.iter().enumerate() {
assert!(
sym == " " || sym == "┃",
"column 9 row {y} should be track ` ` or thumb `┃`, got {sym:?}"
);
assert_eq!(
*bg,
crate::style::Color::Rgb(169, 169, 169),
"row {y} should have track bg (DarkGray)"
);
}
}
#[test]
fn overflow_auto_y_hides_scrollbar_when_content_fits() {
let buf = paint_with_scroll_content(None, Some(Overflow::Auto), 5, 3);
for y in 0..6 {
let bg = buf.cell(9, y).unwrap().bg;
assert_ne!(
bg,
crate::style::Color::Rgb(169, 169, 169),
"auto + fits should not paint scrollbar bg at row {y}"
);
}
}
#[test]
fn overflow_auto_y_shows_scrollbar_when_content_overflows() {
let buf = paint_with_scroll_content(None, Some(Overflow::Auto), 5, 100);
let mut has_scrollbar = false;
for y in 0..6 {
let c = buf.cell(9, y).unwrap();
if c.bg == crate::style::Color::Rgb(169, 169, 169)
&& (c.symbol() == " " || c.symbol() == "┃")
{
has_scrollbar = true;
}
}
assert!(has_scrollbar, "auto + overflow should paint scrollbar");
}
#[test]
fn overflow_scroll_x_paints_horizontal_scrollbar_at_bottom_row() {
let buf = paint_with_scroll_content(Some(Overflow::Scroll), None, 5, 5);
for x in 0..10 {
let c = buf.cell(x, 5).unwrap();
let sym = c.symbol();
assert!(
sym == " " || sym == "━",
"row 5 col {x} should be track ` ` or horizontal thumb `━`, got {sym:?}"
);
assert_eq!(
c.bg,
crate::style::Color::Rgb(169, 169, 169),
"row 5 col {x} should have scrollbar bg"
);
}
}
#[test]
fn both_axes_scroll_leaves_corner_unpainted() {
let buf = paint_with_scroll_content(Some(Overflow::Scroll), Some(Overflow::Scroll), 50, 50);
let corner_bg = buf.cell(9, 5).unwrap().bg;
assert_ne!(
corner_bg,
crate::style::Color::Rgb(169, 169, 169),
"corner should not have scrollbar bg"
);
}
#[test]
fn overflow_x_hidden_does_not_reserve_gutter() {
use crate::style::TuiStyle;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(6))
.overflow_x(Overflow::Hidden)
.overflow_y(Overflow::Hidden),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 12, 8));
for x in 0..12 {
for y in 0..8 {
let bg = buf.cell(x, y).unwrap().bg;
assert_ne!(
bg,
crate::style::Color::Rgb(169, 169, 169),
"hidden overflow should paint no scrollbar at ({x},{y})"
);
}
}
}
#[test]
fn vertical_scrollbar_thumb_paints_heavy_vertical_by_default() {
let buf = paint_with_scroll_content(None, Some(Overflow::Auto), 5, 100);
let thumb_found = (0..6).any(|y| buf.cell(9, y).unwrap().symbol() == "┃");
assert!(
thumb_found,
"vertical thumb should paint with heavy-vertical glyph `┃` by default"
);
for y in 0..6 {
assert_ne!(
buf.cell(9, y).unwrap().symbol(),
"━",
"vertical scrollbar must NOT use horizontal glyph at row {y}"
);
}
}
#[test]
fn horizontal_scrollbar_thumb_paints_heavy_horizontal_by_default() {
let buf = paint_with_scroll_content(Some(Overflow::Auto), None, 100, 5);
let thumb_found = (0..10).any(|x| buf.cell(x, 5).unwrap().symbol() == "━");
assert!(
thumb_found,
"horizontal thumb should paint with heavy-horizontal glyph `━` by default"
);
for x in 0..10 {
assert_ne!(
buf.cell(x, 5).unwrap().symbol(),
"┃",
"horizontal scrollbar must NOT use vertical glyph at col {x}"
);
}
}
#[test]
fn author_content_override_on_scrollbar_thumb_applies_to_both_axes() {
use crate::style::TuiStyle;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(6))
.overflow(Overflow::Scroll),
)
.rule_unchecked(
"c::scrollbar-thumb",
TuiStyle::new().content(Content::Str("█".into())),
);
let _ = pipeline(&mut dom, &sheet, Rect::new(0, 0, 12, 8));
if let Some(ext) = dom.node_mut(c).ext_mut() {
ext.scroll_content_width = 100;
ext.scroll_content_height = 100;
}
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 8));
dom.paint_dom(&mut buf, Rect::new(0, 0, 12, 8));
let v_thumb = (0..5).any(|y| buf.cell(9, y).unwrap().symbol() == "█");
assert!(v_thumb, "author content `█` applies to vertical thumb");
let h_thumb = (0..9).any(|x| buf.cell(x, 5).unwrap().symbol() == "█");
assert!(h_thumb, "author content `█` applies to horizontal thumb");
}
#[test]
fn cross_axis_independence_v1() {
use crate::style::{CascadeExt, TuiStyle};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
dom.append_child(root, c).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("c", TuiStyle::new().overflow_y(Overflow::Scroll));
dom.cascade(&sheet);
let c_computed = dom.node(c).computed().cloned().unwrap();
assert_eq!(c_computed.overflow_y, Overflow::Scroll);
assert_eq!(c_computed.overflow_x, Overflow::Visible);
}
#[test]
fn input_type_text_paints_value_verbatim() {
let mut dom = TuiDom::new();
let root = dom.root();
let input = dom.create_element("input");
dom.set_attribute(input, "value", "hello").unwrap();
dom.append_child(root, input).unwrap();
crate::runtime::builtins::input::seed_all(&mut dom);
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim(), "hello");
}
#[test]
fn input_type_password_masks_value_with_bullets() {
let mut dom = TuiDom::new();
let root = dom.root();
let input = dom.create_element("input");
dom.set_attribute(input, "type", "password").unwrap();
dom.set_attribute(input, "value", "secret").unwrap();
dom.append_child(root, input).unwrap();
crate::runtime::builtins::input::seed_all(&mut dom);
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert_eq!(
row(&buf, 0).trim(),
"\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}"
);
assert_eq!(dom.node(input).get_attribute("value"), Some("secret"));
}
#[test]
fn input_type_email_does_not_mask() {
let mut dom = TuiDom::new();
let root = dom.root();
let input = dom.create_element("input");
dom.set_attribute(input, "type", "email").unwrap();
dom.set_attribute(input, "value", "a@b.c").unwrap();
dom.append_child(root, input).unwrap();
crate::runtime::builtins::input::seed_all(&mut dom);
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim(), "a@b.c");
}
#[test]
fn unchecked_checkbox_renders_empty_box_glyph_via_ua_before_content() {
let mut dom = TuiDom::new();
let root = dom.root();
let cb = dom.create_element("input");
dom.set_attribute(cb, "type", "checkbox").unwrap();
dom.append_child(root, cb).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "[ ]");
}
#[test]
fn checked_checkbox_renders_x_glyph_via_ua_before_content() {
let mut dom = TuiDom::new();
let root = dom.root();
let cb = dom.create_element("input");
dom.set_attribute(cb, "type", "checkbox").unwrap();
dom.set_attribute(cb, "checked", "").unwrap();
dom.append_child(root, cb).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "[x]");
}
#[test]
fn unchecked_radio_renders_empty_circle_glyph() {
let mut dom = TuiDom::new();
let root = dom.root();
let r = dom.create_element("input");
dom.set_attribute(r, "type", "radio").unwrap();
dom.append_child(root, r).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "( )");
}
#[test]
fn checked_radio_renders_filled_circle_glyph() {
let mut dom = TuiDom::new();
let root = dom.root();
let r = dom.create_element("input");
dom.set_attribute(r, "type", "radio").unwrap();
dom.set_attribute(r, "checked", "").unwrap();
dom.append_child(root, r).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "(\u{2022})");
}
#[test]
fn absolute_right_after_pseudo_pins_to_host_right_edge() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
let label = dom.create_text_node("Hello");
dom.append_child(host, label).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"host",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(20))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"host::after",
TuiStyle::new()
.content(Content::Str("]".into()))
.position(crate::layout::Position::Absolute)
.right(crate::layout::Length::Cells(0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 1));
let row0 = row(&buf, 0);
assert!(
row0.starts_with("Hello"),
"host text at start, got {row0:?}"
);
assert_eq!(
buf.cell(19, 0).unwrap().symbol(),
"]",
"absolute position right=0 must pin to host's right edge"
);
}
#[test]
fn absolute_left_before_pseudo_pins_to_host_left_edge() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
let label = dom.create_text_node("text");
dom.append_child(host, label).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"host",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"host::before",
TuiStyle::new()
.content(Content::Str("[".into()))
.position(crate::layout::Position::Absolute)
.left(crate::layout::Length::Cells(0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(
buf.cell(0, 0).unwrap().symbol(),
"[",
"absolute position left=0 pins to host's left edge"
);
}
#[test]
fn static_pseudo_still_paints_inline() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
let label = dom.create_text_node("hi");
dom.append_child(host, label).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked("host", TuiStyle::new().display(Display::InlineBlock))
.rule_unchecked(
"host::before",
TuiStyle::new().content(Content::Str("[".into())),
)
.rule_unchecked(
"host::after",
TuiStyle::new().content(Content::Str("]".into())),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), "[hi]");
}
#[test]
fn display_none_host_suppresses_positioned_pseudo() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked("host", TuiStyle::new().display(Display::None))
.rule_unchecked(
"host::before",
TuiStyle::new()
.content(Content::Str("[".into()))
.position(crate::layout::Position::Absolute)
.left(crate::layout::Length::Cells(0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "");
}
#[test]
fn positioned_pseudo_clips_at_right_viewport_edge() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"host",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(5))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"host::after",
TuiStyle::new()
.content(Content::Str("ABCDE".into()))
.position(crate::layout::Position::Absolute)
.left(crate::layout::Length::Cells(3)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 6, 1));
assert_eq!(buf.cell(3, 0).unwrap().symbol(), "A");
assert_eq!(buf.cell(4, 0).unwrap().symbol(), "B");
assert_eq!(buf.cell(5, 0).unwrap().symbol(), "C");
}
#[test]
fn positioned_pseudo_entirely_off_left_edge_paints_nothing() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
let label = dom.create_text_node("host");
dom.append_child(host, label).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"host",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(5))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"host::after",
TuiStyle::new()
.content(Content::Str("XYZ".into()))
.position(crate::layout::Position::Absolute)
.right(crate::layout::Length::Cells(50)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "host");
}
#[test]
fn zero_width_positioned_pseudo_paints_nothing() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
let label = dom.create_text_node("host");
dom.append_child(host, label).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"host",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(10))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"host::before",
TuiStyle::new()
.content(Content::Str("".into()))
.position(crate::layout::Position::Absolute)
.left(crate::layout::Length::Cells(0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(row(&buf, 0).trim_end(), "host");
}
#[test]
fn auto_width_element_with_pseudo_chrome_sizes_to_include_pseudo_content() {
let mut dom = TuiDom::new();
let root = dom.root();
let el = dom.create_element("custom");
let t = dom.create_text_node("Submit");
dom.append_child(el, t).unwrap();
dom.append_child(root, el).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked("custom", TuiStyle::new().display(Display::Block))
.rule_unchecked(
"custom::before",
TuiStyle::new().content(Content::Str("[ ".into())),
)
.rule_unchecked(
"custom::after",
TuiStyle::new().content(Content::Str(" ]".into())),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), "[ Submit ]");
}
#[test]
fn ua_summary_renders_right_triangle_when_closed() {
let mut dom = TuiDom::new();
let root = dom.root();
let details = dom.create_element("details");
let summary = dom.create_element("summary");
let label = dom.create_text_node("Advanced");
dom.append_child(summary, label).unwrap();
dom.append_child(details, summary).unwrap();
dom.append_child(root, details).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), "▸ Advanced");
}
#[test]
fn ua_closed_details_hides_non_summary_children() {
let mut dom = TuiDom::new();
let root = dom.root();
let details = dom.create_element("details");
let summary = dom.create_element("summary");
let s_t = dom.create_text_node("Click to expand");
dom.append_child(summary, s_t).unwrap();
dom.append_child(details, summary).unwrap();
let body = dom.create_element("p");
let b_t = dom.create_text_node("SECRET");
dom.append_child(body, b_t).unwrap();
dom.append_child(details, body).unwrap();
dom.append_child(root, details).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 30, 3));
assert_eq!(row(&buf, 0).trim_end(), "▸ Click to expand");
for y in 0..3 {
assert!(
!row(&buf, y).contains("SECRET"),
"row {y} leaked closed-details content: {:?}",
row(&buf, y)
);
}
}
#[test]
fn ua_summary_renders_down_triangle_when_open() {
let mut dom = TuiDom::new();
let root = dom.root();
let details = dom.create_element("details");
dom.set_attribute(details, "open", "").unwrap();
let summary = dom.create_element("summary");
let label = dom.create_text_node("Advanced");
dom.append_child(summary, label).unwrap();
dom.append_child(details, summary).unwrap();
dom.append_child(root, details).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), "▾ Advanced");
}
#[test]
fn ua_ul_renders_bullet_before_each_li() {
let mut dom = TuiDom::new();
let root = dom.root();
let ul = dom.create_element("ul");
for label in ["first", "second"] {
let li = dom.create_element("li");
let t = dom.create_text_node(label);
dom.append_child(li, t).unwrap();
dom.append_child(ul, li).unwrap();
}
dom.append_child(root, ul).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 3));
assert_eq!(row(&buf, 0).trim_end(), " • first");
assert_eq!(row(&buf, 1).trim_end(), " • second");
}
#[test]
fn ua_ol_renders_bullet_marker() {
let mut dom = TuiDom::new();
let root = dom.root();
let ol = dom.create_element("ol");
let li = dom.create_element("li");
let t = dom.create_text_node("first");
dom.append_child(li, t).unwrap();
dom.append_child(ol, li).unwrap();
dom.append_child(root, ol).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert_eq!(row(&buf, 0).trim_end(), " • first");
}
#[test]
fn progress_renders_block_bar_at_value_max_ratio() {
let mut dom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("progress");
dom.set_attribute(p, "value", "0.5").unwrap();
dom.set_attribute(p, "max", "1").unwrap();
dom.append_child(root, p).unwrap();
let sheet =
Stylesheet::new().rule_unchecked("progress", TuiStyle::new().width(Size::Fixed(10)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(
row(&buf, 0).trim_end(),
"\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}"
);
}
#[test]
fn progress_without_value_renders_indeterminate_track() {
let mut dom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("progress");
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::new().rule_unchecked("progress", TuiStyle::new().width(Size::Fixed(5)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(
row(&buf, 0).trim_end(),
"\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}"
);
}
#[test]
fn anchor_with_href_tags_painted_cells_with_link() {
let mut dom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let a = dom.create_element("a");
dom.set_attribute(a, "href", "https://example.com").unwrap();
let t = dom.create_text_node("click");
dom.append_child(a, t).unwrap();
dom.append_child(p, a).unwrap();
dom.append_child(root, p).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
for x in 0..5 {
assert_eq!(
buf.cell(x, 0).unwrap().link(),
Some("https://example.com"),
"cell {x} missing link"
);
}
}
#[test]
fn anchor_without_href_does_not_tag_cells() {
let mut dom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let a = dom.create_element("a");
let t = dom.create_text_node("plain");
dom.append_child(a, t).unwrap();
dom.append_child(p, a).unwrap();
dom.append_child(root, p).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
assert!(buf.cell(0, 0).unwrap().link().is_none());
}
#[test]
fn nested_inline_inside_anchor_inherits_the_href_link() {
let mut dom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let a = dom.create_element("a");
dom.set_attribute(a, "href", "https://nested.test").unwrap();
let b = dom.create_element("b");
let t = dom.create_text_node("bold");
dom.append_child(b, t).unwrap();
dom.append_child(a, b).unwrap();
dom.append_child(p, a).unwrap();
dom.append_child(root, p).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 10, 1));
for x in 0..4 {
assert_eq!(
buf.cell(x, 0).unwrap().link(),
Some("https://nested.test"),
"cell {x} should carry the anchor's link"
);
}
}
#[test]
fn modal_dialog_backdrop_fills_viewport_with_bg() {
use crate::style::Color;
let mut dom = TuiDom::new();
let root = dom.root();
let bg_el = dom.create_element("p");
let bg_text = dom.create_text_node("under");
dom.append_child(bg_el, bg_text).unwrap();
dom.append_child(root, bg_el).unwrap();
let dlg = dom.create_element("dialog");
dom.set_attribute(dlg, "open", "").unwrap();
dom.set_attribute(dlg, "data-rdom-modal", "").unwrap();
dom.append_child(root, dlg).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"dialog::backdrop",
TuiStyle::new().bg(Color::Rgb(169, 169, 169)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 30, 10));
for y in 5..10 {
for x in 0..30 {
assert_eq!(
buf.cell(x, y).unwrap().bg,
Color::Rgb(169, 169, 169),
"cell ({x},{y}) should have backdrop bg"
);
}
}
assert_eq!(buf.cell(0, 1).unwrap().symbol(), "╭");
}
#[test]
fn non_modal_dialog_has_no_backdrop() {
use crate::style::Color;
let mut dom = TuiDom::new();
let root = dom.root();
let dlg = dom.create_element("dialog");
dom.set_attribute(dlg, "open", "").unwrap();
dom.append_child(root, dlg).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"dialog::backdrop",
TuiStyle::new().bg(Color::Rgb(255, 0, 0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(buf.cell(0, 0).unwrap().bg, Color::Reset);
}
#[test]
fn closed_dialog_has_no_backdrop() {
use crate::style::Color;
let mut dom = TuiDom::new();
let root = dom.root();
let dlg = dom.create_element("dialog");
dom.set_attribute(dlg, "data-rdom-modal", "").unwrap();
dom.append_child(root, dlg).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"dialog::backdrop",
TuiStyle::new().bg(Color::Rgb(255, 0, 0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(buf.cell(0, 0).unwrap().bg, Color::Reset);
}
#[test]
fn modal_dialog_repaints_over_backdrop() {
use crate::style::Color;
let mut dom = TuiDom::new();
let root = dom.root();
let dlg = dom.create_element("dialog");
dom.set_attribute(dlg, "open", "").unwrap();
dom.set_attribute(dlg, "data-rdom-modal", "").unwrap();
let t = dom.create_text_node("hi");
dom.append_child(dlg, t).unwrap();
dom.append_child(root, dlg).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"dialog::backdrop",
TuiStyle::new().bg(Color::Rgb(169, 169, 169)),
)
.rule_unchecked("dialog", TuiStyle::new().bg(Color::Rgb(0, 0, 255)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(buf.cell(3, 2).unwrap().bg, Color::Rgb(0, 0, 255));
assert_eq!(buf.cell(15, 9).unwrap().bg, Color::Rgb(169, 169, 169));
}
#[test]
fn table_renders_rows_stacked_vertically_cells_horizontally() {
let mut dom = TuiDom::new();
let root = dom.root();
let table = dom.create_element("table");
let tbody = dom.create_element("tbody");
for (name, age) in [("Alice", "30"), ("Bob", "25")] {
let tr = dom.create_element("tr");
let td1 = dom.create_element("td");
let t1 = dom.create_text_node(name);
dom.append_child(td1, t1).unwrap();
let td2 = dom.create_element("td");
let t2 = dom.create_text_node(age);
dom.append_child(td2, t2).unwrap();
dom.append_child(tr, td1).unwrap();
dom.append_child(tr, td2).unwrap();
dom.append_child(tbody, tr).unwrap();
}
dom.append_child(table, tbody).unwrap();
dom.append_child(root, table).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 3));
let r0 = row(&buf, 0);
let r1 = row(&buf, 1);
assert!(r0.contains("Alice") && r0.contains("30"), "row 0: {r0:?}");
assert!(r1.contains("Bob") && r1.contains("25"), "row 1: {r1:?}");
}
#[test]
fn table_header_cells_are_rendered_bold_via_ua_style() {
use crate::node::TuiNodeExt;
use crate::style::CascadeExt;
let mut dom = TuiDom::new();
let root = dom.root();
let table = dom.create_element("table");
let thead = dom.create_element("thead");
let tr = dom.create_element("tr");
let th = dom.create_element("th");
let t = dom.create_text_node("Name");
dom.append_child(th, t).unwrap();
dom.append_child(tr, th).unwrap();
dom.append_child(thead, tr).unwrap();
dom.append_child(table, thead).unwrap();
dom.append_child(root, table).unwrap();
dom.cascade(&Stylesheet::new());
let computed = dom.node(th).computed().cloned().unwrap();
assert!(computed.modifiers.contains(crate::style::Modifier::BOLD));
}
#[test]
fn table_caption_uses_italic_dim_style() {
use crate::node::TuiNodeExt;
use crate::style::CascadeExt;
let mut dom = TuiDom::new();
let root = dom.root();
let table = dom.create_element("table");
let caption = dom.create_element("caption");
let t = dom.create_text_node("My Table");
dom.append_child(caption, t).unwrap();
dom.append_child(table, caption).unwrap();
dom.append_child(root, table).unwrap();
dom.cascade(&Stylesheet::new());
let computed = dom.node(caption).computed().cloned().unwrap();
assert!(computed.modifiers.contains(crate::style::Modifier::ITALIC));
assert_eq!(computed.fg, crate::style::Color::Rgb(127, 134, 139));
}
#[test]
fn bare_tr_without_tbody_still_renders() {
let mut dom = TuiDom::new();
let root = dom.root();
let table = dom.create_element("table");
let tr = dom.create_element("tr");
let td = dom.create_element("td");
let t = dom.create_text_node("Direct");
dom.append_child(td, t).unwrap();
dom.append_child(tr, td).unwrap();
dom.append_child(table, tr).unwrap();
dom.append_child(root, table).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 1));
assert!(row(&buf, 0).contains("Direct"));
}
#[test]
fn table_column_sync_makes_cells_align_across_rows() {
let mut dom = TuiDom::new();
let root = dom.root();
let table = dom.create_element("table");
let tbody = dom.create_element("tbody");
for (a, b) in [("Alice", "30"), ("Bo", "251")] {
let tr = dom.create_element("tr");
for text in [a, b] {
let td = dom.create_element("td");
let t = dom.create_text_node(text);
dom.append_child(td, t).unwrap();
dom.append_child(tr, td).unwrap();
}
dom.append_child(tbody, tr).unwrap();
}
dom.append_child(table, tbody).unwrap();
dom.append_child(root, table).unwrap();
crate::runtime::builtins::table::size_all_tables(&mut dom);
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 30, 2));
let r0 = row(&buf, 0);
let r1 = row(&buf, 1);
let i30 = r0.find("30").expect("row 0 has '30'");
let i251 = r1.find("251").expect("row 1 has '251'");
assert_eq!(i30, i251, "r0={r0:?} r1={r1:?}");
}
#[test]
fn colgroup_and_col_do_not_render_visible_content() {
let mut dom = TuiDom::new();
let root = dom.root();
let table = dom.create_element("table");
let colgroup = dom.create_element("colgroup");
let col1 = dom.create_element("col");
let col2 = dom.create_element("col");
dom.append_child(colgroup, col1).unwrap();
dom.append_child(colgroup, col2).unwrap();
dom.append_child(table, colgroup).unwrap();
let tr = dom.create_element("tr");
let td = dom.create_element("td");
let t = dom.create_text_node("X");
dom.append_child(td, t).unwrap();
dom.append_child(tr, td).unwrap();
dom.append_child(table, tr).unwrap();
dom.append_child(root, table).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 2));
assert!(
row(&buf, 0).contains("X"),
"row 0 should contain X, got {:?}",
row(&buf, 0)
);
}
#[test]
fn closed_dropdown_renders_arrow_plus_selected_label() {
let mut dom = TuiDom::new();
let root = dom.root();
let sel = dom.create_element("select");
let o1 = dom.create_element("option");
dom.set_attribute(o1, "value", "dog").unwrap();
dom.set_attribute(o1, "selected", "").unwrap();
let t1 = dom.create_text_node("Dog");
dom.append_child(o1, t1).unwrap();
let o2 = dom.create_element("option");
let t2 = dom.create_text_node("Cat");
dom.append_child(o2, t2).unwrap();
dom.append_child(sel, o1).unwrap();
dom.append_child(sel, o2).unwrap();
dom.append_child(root, sel).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 3));
let r0 = row(&buf, 0);
assert!(
r0.contains("Dog"),
"selected option label echoed; got {r0:?}"
);
assert!(
r0.contains('\u{25BE}'),
"UA `::after` chevron should appear; got {r0:?}"
);
assert_eq!(row(&buf, 1).trim_end(), "");
}
#[test]
fn open_dropdown_renders_options_inline_without_chrome() {
let mut dom = TuiDom::new();
let root = dom.root();
let sel = dom.create_element("select");
crate::runtime::builtins::select::open(&mut dom, sel);
let o1 = dom.create_element("option");
dom.set_attribute(o1, "value", "dog").unwrap();
dom.set_attribute(o1, "selected", "").unwrap();
let t1 = dom.create_text_node("Dog");
dom.append_child(o1, t1).unwrap();
let o2 = dom.create_element("option");
let t2 = dom.create_text_node("Cat");
dom.append_child(o2, t2).unwrap();
dom.append_child(sel, o1).unwrap();
dom.append_child(sel, o2).unwrap();
dom.append_child(root, sel).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 4));
let r0 = row(&buf, 0);
let r1 = row(&buf, 1);
assert!(
r0.contains("Dog"),
"row 0 should include Dog option, got {r0:?}"
);
assert!(
r1.contains("Cat"),
"row 1 should include Cat option, got {r1:?}"
);
}
#[test]
fn listbox_renders_all_options_without_chrome() {
let mut dom = TuiDom::new();
let root = dom.root();
let sel = dom.create_element("select");
dom.set_attribute(sel, "multiple", "").unwrap();
let o1 = dom.create_element("option");
let t1 = dom.create_text_node("Dog");
dom.append_child(o1, t1).unwrap();
let o2 = dom.create_element("option");
let t2 = dom.create_text_node("Cat");
dom.append_child(o2, t2).unwrap();
dom.append_child(sel, o1).unwrap();
dom.append_child(sel, o2).unwrap();
dom.append_child(root, sel).unwrap();
let buf = pipeline(&mut dom, &Stylesheet::new(), Rect::new(0, 0, 20, 4));
assert!(row(&buf, 0).contains("Dog"));
assert!(row(&buf, 1).contains("Cat"));
}
#[test]
fn meter_renders_block_bar_at_value_in_min_max_range() {
let mut dom = TuiDom::new();
let root = dom.root();
let m = dom.create_element("meter");
dom.set_attribute(m, "min", "0").unwrap();
dom.set_attribute(m, "max", "100").unwrap();
dom.set_attribute(m, "value", "30").unwrap();
dom.append_child(root, m).unwrap();
let sheet = Stylesheet::new().rule_unchecked("meter", TuiStyle::new().width(Size::Fixed(10)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
assert_eq!(
row(&buf, 0).trim_end(),
"\u{2588}\u{2588}\u{2588}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}"
);
}
fn bg_at(buf: &Buffer, x: u16, y: u16) -> Color {
buf.cell(x, y).map(|c| c.bg).unwrap_or(Color::Reset)
}
#[test]
fn higher_z_index_paints_on_top() {
let mut dom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
dom.append_child(root, c).unwrap();
let base = || {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1))
};
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
base()
.bg(Color::Rgb(255, 0, 0))
.z_index(crate::layout::ZIndex::Value(1)),
)
.rule_unchecked(
"b",
base()
.bg(Color::Rgb(0, 0, 255))
.z_index(crate::layout::ZIndex::Value(5)),
)
.rule_unchecked(
"c",
base()
.bg(Color::Rgb(0, 128, 0))
.z_index(crate::layout::ZIndex::Value(3)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(
bg_at(&buf, 0, 0),
Color::Rgb(0, 0, 255),
"z=5 should be on top"
);
}
#[test]
fn z_index_auto_paints_in_document_order() {
let mut dom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
let base = || {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1))
};
let sheet = Stylesheet::bare()
.rule_unchecked("a", base().bg(Color::Rgb(255, 0, 0)))
.rule_unchecked("b", base().bg(Color::Rgb(0, 0, 255)));
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(bg_at(&buf, 0, 0), Color::Rgb(0, 0, 255));
}
#[test]
fn positioned_paints_above_in_flow_content() {
let mut dom = TuiDom::new();
let root = dom.root();
let bar = dom.create_element("bar");
let tip = dom.create_element("tip");
dom.append_child(root, tip).unwrap(); dom.append_child(root, bar).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"bar",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(1))
.bg(Color::Rgb(255, 0, 0)),
)
.rule_unchecked(
"tip",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1))
.bg(Color::Rgb(0, 0, 255)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(bg_at(&buf, 0, 0), Color::Rgb(0, 0, 255));
assert_eq!(bg_at(&buf, 4, 0), Color::Rgb(255, 0, 0));
}
#[test]
fn negative_z_index_paints_before_zero() {
let mut dom = TuiDom::new();
let root = dom.root();
let lo = dom.create_element("lo");
let hi = dom.create_element("hi");
dom.append_child(root, lo).unwrap();
dom.append_child(root, hi).unwrap();
let base = || {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1))
};
let sheet = Stylesheet::bare()
.rule_unchecked(
"lo",
base()
.bg(Color::Rgb(255, 0, 0))
.z_index(crate::layout::ZIndex::Value(-1)),
)
.rule_unchecked(
"hi",
base()
.bg(Color::Rgb(0, 0, 255))
.z_index(crate::layout::ZIndex::Value(0)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(bg_at(&buf, 0, 0), Color::Rgb(0, 0, 255));
}
#[test]
fn higher_z_paints_on_top_of_lower_z() {
let mut dom = TuiDom::new();
let root = dom.root();
let lo = dom.create_element("lo");
let hi = dom.create_element("hi");
dom.append_child(root, lo).unwrap();
dom.append_child(root, hi).unwrap();
let base = || {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1))
};
let sheet = Stylesheet::bare()
.rule_unchecked(
"lo",
base()
.bg(Color::Rgb(255, 0, 0))
.z_index(crate::layout::ZIndex::Value(10)),
)
.rule_unchecked(
"hi",
base()
.bg(Color::Rgb(0, 0, 255))
.z_index(crate::layout::ZIndex::Value(2)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(bg_at(&buf, 0, 0), Color::Rgb(255, 0, 0));
}
#[test]
fn opacity_half_blends_fg_against_black_parent_bg() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("p");
let child = dom.create_element("c");
let t = dom.create_text_node("x");
dom.append_child(child, t).unwrap();
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(1))
.bg(Color::Rgb(0, 0, 0)),
)
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(3))
.height(Size::Fixed(1))
.fg(Color::Rgb(255, 0, 0))
.opacity(0.5),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
let cell = buf.cell(0, 0).unwrap();
assert_eq!(cell.symbol(), "x");
assert_eq!(cell.fg, Color::Rgb(128, 0, 0));
}
#[test]
fn opacity_one_does_not_blend() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let c = dom.create_element("c");
let t = dom.create_text_node("x");
dom.append_child(c, t).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(3))
.height(Size::Fixed(1))
.fg(Color::Rgb(255, 0, 0))
.opacity(1.0),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(buf.cell(0, 0).unwrap().fg, Color::Rgb(255, 0, 0));
}
#[test]
fn opacity_one_overlay_occludes_underlying_glyphs() {
let mut dom = TuiDom::new();
let root = dom.root();
let bg_box = dom.create_element("b");
let overlay = dom.create_element("o");
dom.append_child(root, bg_box).unwrap();
dom.append_child(root, overlay).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::rounded())
.border_fg(Color::Rgb(200, 200, 200)),
)
.rule_unchecked(
"o",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(6))
.height(Size::Fixed(1))
.bg(Color::Rgb(61, 144, 206)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
for x in 0..6 {
let cell = buf.cell(x, 0).unwrap();
assert_eq!(
cell.bg,
Color::Rgb(61, 144, 206),
"row 0 col {x}: overlay bg"
);
assert_eq!(
cell.symbol(),
" ",
"row 0 col {x}: overlay must clear underlying border glyph"
);
}
}
#[test]
fn opacity_half_overlay_blends_bg_and_preserves_underlying_glyphs() {
let mut dom = TuiDom::new();
let root = dom.root();
let bg_box = dom.create_element("b");
let overlay = dom.create_element("o");
dom.append_child(root, bg_box).unwrap();
dom.append_child(root, overlay).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::rounded())
.border_fg(Color::Rgb(200, 200, 200)),
)
.rule_unchecked(
"o",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(6))
.height(Size::Fixed(1))
.bg(Color::Rgb(200, 100, 0))
.opacity(0.5),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
for x in 0..6 {
let cell = buf.cell(x, 0).unwrap();
assert_eq!(
cell.bg,
Color::Rgb(100, 50, 0),
"row 0 col {x}: blended bg at 50% opacity"
);
}
assert_eq!(
buf.cell(0, 0).unwrap().symbol(),
"╭",
"left border bleeds through translucent overlay"
);
assert_eq!(
buf.cell(5, 0).unwrap().symbol(),
"╮",
"right border bleeds through translucent overlay"
);
assert_eq!(
buf.cell(2, 0).unwrap().symbol(),
"─",
"top horizontal border bleeds through translucent overlay"
);
}
#[test]
fn opacity_zero_overlay_is_invisible_keeps_symbols() {
let mut dom = TuiDom::new();
let root = dom.root();
let bg_box = dom.create_element("b");
let overlay = dom.create_element("o");
dom.append_child(root, bg_box).unwrap();
dom.append_child(root, overlay).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::rounded())
.border_fg(Color::Rgb(200, 200, 200)),
)
.rule_unchecked(
"o",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(6))
.height(Size::Fixed(1))
.bg(Color::Rgb(200, 100, 0))
.opacity(0.0),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "╭");
assert_eq!(buf.cell(2, 0).unwrap().symbol(), "─");
assert_eq!(buf.cell(5, 0).unwrap().symbol(), "╮");
}
#[test]
fn translucent_card_own_text_does_not_double_blend_bg() {
let mut dom = TuiDom::new();
let root = dom.root();
let card = dom.create_element("c");
let txt = dom.create_text_node("hi");
dom.append_child(card, txt).unwrap();
dom.append_child(root, card).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(8))
.height(Size::Fixed(1))
.bg(Color::Rgb(200, 0, 0))
.opacity(0.5),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 10, 1));
let bg_under_text = buf.cell(0, 0).unwrap().bg;
let bg_no_text = buf.cell(2, 0).unwrap().bg;
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "h");
assert_eq!(buf.cell(2, 0).unwrap().symbol(), " ");
assert_eq!(
bg_under_text, bg_no_text,
"text cells must share the bg of non-text cells (no double-blend)"
);
assert_eq!(
bg_under_text,
Color::Rgb(100, 0, 0),
"expected single-blend (100,0,0); got {bg_under_text:?}"
);
}
#[test]
fn translucent_overlay_blends_against_actual_cell_bg_not_parent_bg() {
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("h");
let lower = dom.create_element("lo");
let upper = dom.create_element("up");
dom.append_child(host, lower).unwrap();
dom.append_child(host, upper).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"h",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.position(crate::layout::Position::Relative),
)
.rule_unchecked(
"lo",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(8))
.height(Size::Fixed(3))
.bg(Color::Rgb(255, 0, 0))
.z_index(crate::layout::ZIndex::Value(1)),
)
.rule_unchecked(
"up",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(8))
.height(Size::Fixed(3))
.bg(Color::Rgb(0, 0, 255))
.opacity(0.5)
.z_index(crate::layout::ZIndex::Value(2)),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 12, 4));
for x in 0..8 {
for y in 0..3 {
let cell = buf.cell(x, y).unwrap();
assert_eq!(
cell.bg,
Color::Rgb(128, 0, 128),
"cell ({x},{y}): expected magenta from blend against cell.bg=red, got {:?}",
cell.bg
);
}
}
}
#[test]
fn opacity_zero_collapses_fg_to_parent_bg() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("p");
let child = dom.create_element("c");
let t = dom.create_text_node("x");
dom.append_child(child, t).unwrap();
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(1))
.bg(Color::Rgb(50, 100, 150)),
)
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(3))
.height(Size::Fixed(1))
.fg(Color::Rgb(255, 0, 0))
.opacity(0.0),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 5, 1));
assert_eq!(buf.cell(0, 0).unwrap().fg, Color::Rgb(50, 100, 150));
}
#[test]
fn collapse_two_bordered_siblings_render_t_junctions() {
use rdom_style::layout::{Border, BorderCollapse};
let mut dom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(outer, a).unwrap();
dom.append_child(outer, b).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(11))
.height(Size::Fixed(3))
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single())
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::single()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::single()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 11, 3));
assert_eq!(buf.cell(0, 0).unwrap().symbol(), "┌");
assert_eq!(buf.cell(5, 0).unwrap().symbol(), "┬", "top junction");
assert_eq!(buf.cell(10, 0).unwrap().symbol(), "┐");
assert_eq!(buf.cell(0, 2).unwrap().symbol(), "└");
assert_eq!(buf.cell(5, 2).unwrap().symbol(), "┴", "bottom junction");
assert_eq!(buf.cell(10, 2).unwrap().symbol(), "┘");
assert_eq!(buf.cell(5, 1).unwrap().symbol(), "│");
}
#[test]
fn collapse_joiner_is_noop_when_no_element_has_collapse() {
use rdom_style::layout::Border;
let mut dom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(outer, a).unwrap();
dom.append_child(outer, b).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(14))
.height(Size::Fixed(3))
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single()),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::single()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(3))
.border(Border::single()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 14, 3));
assert_eq!(buf.cell(1, 0).unwrap().symbol(), "─");
assert_eq!(buf.cell(7, 0).unwrap().symbol(), "─");
assert_eq!(buf.cell(1, 1).unwrap().symbol(), "┌");
assert_eq!(buf.cell(0, 1).unwrap().symbol(), "│");
}
#[test]
fn collapse_three_sibling_nested_grid_renders_correct_junctions() {
use rdom_style::layout::{Border, BorderCollapse};
let mut dom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("outer");
let row = dom.create_element("row");
let left = dom.create_element("left");
let mid = dom.create_element("mid");
let right = dom.create_element("right");
dom.append_child(row, left).unwrap();
dom.append_child(row, mid).unwrap();
dom.append_child(row, right).unwrap();
dom.append_child(outer, row).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(15))
.height(Size::Fixed(3))
.border(Border::single())
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked(
"row",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single())
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked(
"left",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(3))
.border(Border::single()),
)
.rule_unchecked(
"mid",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(3))
.border(Border::single()),
)
.rule_unchecked(
"right",
TuiStyle::new()
.width(Size::Fixed(5))
.height(Size::Fixed(3))
.border(Border::single()),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 15, 3));
assert_eq!(
buf.cell(4, 0).unwrap().symbol(),
"┬",
"first sibling boundary at top"
);
assert_eq!(
buf.cell(8, 0).unwrap().symbol(),
"┬",
"second sibling boundary at top"
);
assert_eq!(
buf.cell(4, 2).unwrap().symbol(),
"┴",
"first sibling boundary at bottom"
);
assert_eq!(
buf.cell(8, 2).unwrap().symbol(),
"┴",
"second sibling boundary at bottom"
);
}
#[test]
fn text_decoration_line_through_paints_crossed_out_modifier_on_cells() {
use crate::layout::TextDecoration;
let mut dom = TuiDom::new();
let root = dom.root();
let host = dom.create_element("host");
let txt = dom.create_text_node("xy");
dom.append_child(host, txt).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"host",
TuiStyle::new()
.display(Display::Block)
.text_decoration(TextDecoration::LineThrough),
);
let buf = pipeline(&mut dom, &sheet, Rect::new(0, 0, 4, 1));
for x in 0..2 {
let cell = buf.cell(x, 0).expect("cell present");
assert!(
cell.modifier.contains(Modifier::CROSSED_OUT),
"cell ({x}, 0) {:?} missing CROSSED_OUT — paint dropped the modifier",
cell.modifier
);
}
}