#![cfg(test)]
use rdom_core::Dom;
use crate::ext::TuiExt;
use crate::layout::{Border, LayoutRect, Margin, MarginValue, Padding, PaddingValue, Size};
use crate::node::TuiNodeExt;
use crate::prelude::*;
use crate::render::layout_pass::block::layout_block_children;
use crate::style::ComputedStyle;
fn dom() -> TuiDom {
TuiDom::new()
}
fn cascade(dom: &mut TuiDom, sheet: &Stylesheet) {
dom.cascade(sheet);
}
fn layout_of(dom: &TuiDom, id: NodeId) -> LayoutRect {
dom.node(id).ext().unwrap().layout
}
fn run_block(dom: &mut TuiDom, container_id: NodeId, container_rect: LayoutRect) {
let computed = dom
.node(container_id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
layout_block_children(
dom as &mut Dom<TuiExt>,
container_id,
container_rect,
&computed,
);
}
#[test]
fn block_child_with_width_auto_fills_containing_block() {
let mut dom = dom();
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(); cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(r.x, 0);
assert_eq!(r.width, 80, "auto width fills containing block");
}
#[test]
fn block_child_with_width_fixed_respects_declared() {
let mut dom = dom();
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("c", TuiStyle::new().width(Size::Fixed(30)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, child).width, 30);
}
#[test]
fn block_child_with_width_percent_resolves_against_containing_block() {
let mut dom = dom();
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("c", TuiStyle::new().width(Size::Percent(50)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, child).width,
40,
"50% of 80-cell containing block = 40"
);
}
#[test]
fn block_child_with_explicit_left_right_margins_shrinks_auto_width() {
let mut dom = dom();
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(
"c",
TuiStyle::new().margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(5),
MarginValue::Cells(0),
MarginValue::Cells(3),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(r.x, 3, "left margin shifts x");
assert_eq!(r.width, 72, "width = 80 - 3 left margin - 5 right margin");
}
#[test]
fn auto_margin_left_pushes_block_to_right_edge() {
let mut dom = dom();
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(
"c",
TuiStyle::new().width(Size::Fixed(20)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Auto,
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(
r.x, 60,
"margin-left: auto absorbs 60 cells, pushing child to x=60"
);
assert_eq!(r.width, 20);
}
#[test]
fn auto_margin_right_pushes_block_to_left_edge() {
let mut dom = dom();
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(
"c",
TuiStyle::new().width(Size::Fixed(20)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Auto,
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(r.x, 0, "child stays at x=0");
assert_eq!(r.width, 20);
}
#[test]
fn both_auto_margins_center_block() {
let mut dom = dom();
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(
"c",
TuiStyle::new().width(Size::Fixed(40)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Auto,
MarginValue::Cells(0),
MarginValue::Auto,
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(r.x, 20, "centered: (80 - 40) / 2 = 20");
assert_eq!(r.width, 40);
}
#[test]
fn both_auto_margins_with_odd_leftover_distribute_odd_cell_to_right() {
let mut dom = dom();
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(
"c",
TuiStyle::new().width(Size::Fixed(41)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Auto,
MarginValue::Cells(0),
MarginValue::Auto,
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(
r.x, 19,
"odd leftover splits 19/20 (right gets the odd cell)"
);
}
#[test]
fn max_width_clamps_block_smaller() {
let mut dom = dom();
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("c", TuiStyle::new().width(Size::Fixed(50)).max_width(30));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, child).width, 30, "max-width clamps");
}
#[test]
fn min_width_floors_block_larger() {
use crate::layout::MinSize;
let mut dom = dom();
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(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.min_width(MinSize::Cells(25)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, child).width, 25, "min-width floors");
}
#[test]
fn block_children_stack_vertically_in_document_order() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(parent, c).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(2)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(3)))
.rule_unchecked("c", TuiStyle::new().height(Size::Fixed(1)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, a).y, 0);
assert_eq!(layout_of(&dom, a).height, 2);
assert_eq!(layout_of(&dom, b).y, 2, "b stacks below a (no margin yet)");
assert_eq!(layout_of(&dom, b).height, 3);
assert_eq!(layout_of(&dom, c).y, 5, "c stacks below b");
}
#[test]
fn block_children_overflow_below_container() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(20)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(20)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 10));
assert_eq!(layout_of(&dom, a).height, 20);
assert_eq!(layout_of(&dom, b).y, 20);
assert_eq!(layout_of(&dom, b).height, 20);
}
#[test]
fn adjacent_positive_margins_collapse_to_max() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(2),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, a).y, 0);
assert_eq!(
layout_of(&dom, b).y,
2 + 3,
"A bottom (2) + max(A.mb=2, B.mt=3)=3 = 5"
);
}
#[test]
fn adjacent_margins_equal_collapse_to_one() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, b).y,
1 + 3,
"1 (A height) + 3 (collapsed margin) = 4"
);
}
#[test]
fn three_adjacent_siblings_collapse_each_gap_independently() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(parent, c).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(2),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(4),
MarginValue::Cells(0),
MarginValue::Cells(1),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, a).y, 0);
assert_eq!(layout_of(&dom, b).y, 1 + 4, "after A: 1 + max(2, 4) = 5");
assert_eq!(
layout_of(&dom, c).y,
1 + 4 + 1 + 2,
"after B: prev (5) + B.h (1) + max(B.mb=1, C.mt=2) = 8"
);
}
#[test]
fn parent_outer_top_margin_propagates_child_margin_upward_to_grandparent() {
let mut dom = dom();
let root = dom.root();
let gp = dom.create_element("gp");
let a = dom.create_element("a");
let p = dom.create_element("p");
let c = dom.create_element("c");
dom.append_child(p, c).unwrap();
dom.append_child(gp, a).unwrap();
dom.append_child(gp, p).unwrap();
dom.append_child(root, gp).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked(
"p",
TuiStyle::new().height(Size::Fixed(10)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(5),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, gp, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, p).y,
1 + 5,
"P.outer_top includes max(P.mt=2, C.mt=5) = 5 → P.y = A.bottom (1) + 5 = 6"
);
assert_eq!(
layout_of(&dom, c).y,
layout_of(&dom, p).y,
"C is suppressed inside P — sits at P.y"
);
}
#[test]
fn parent_outer_top_margin_propagates_through_multilevel_chain() {
let mut dom = dom();
let root = dom.root();
let gp = dom.create_element("gp");
let a = dom.create_element("a");
let m = dom.create_element("m");
let p = dom.create_element("p");
let c = dom.create_element("c");
dom.append_child(p, c).unwrap();
dom.append_child(m, p).unwrap();
dom.append_child(gp, a).unwrap();
dom.append_child(gp, m).unwrap();
dom.append_child(root, gp).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked(
"m",
TuiStyle::new().height(Size::Fixed(10)).margin(Margin::new(
MarginValue::Cells(1),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"p",
TuiStyle::new().height(Size::Fixed(8)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(7),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, gp, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, m).y,
1 + 7,
"M.outer_top includes max(M.mt=1, P.mt=3, C.mt=7) = 7"
);
}
#[test]
fn parent_outer_top_chain_stops_at_padding() {
let mut dom = dom();
let root = dom.root();
let gp = dom.create_element("gp");
let a = dom.create_element("a");
let m = dom.create_element("m");
let p = dom.create_element("p");
let c = dom.create_element("c");
dom.append_child(p, c).unwrap();
dom.append_child(m, p).unwrap();
dom.append_child(gp, a).unwrap();
dom.append_child(gp, m).unwrap();
dom.append_child(root, gp).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked(
"m",
TuiStyle::new().height(Size::Fixed(15)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"p",
TuiStyle::new()
.height(Size::Fixed(10))
.padding(Padding::new(2, 0, 0, 0))
.margin(Margin::new(
MarginValue::Cells(4),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(9),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, gp, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, m).y,
1 + 4,
"padding on P blocks chain — M.outer_top = max(2, 4) = 4"
);
assert_eq!(layout_of(&dom, p).y, 1 + 4, "P at M.y (M-P collapse)");
assert_eq!(
layout_of(&dom, c).y,
layout_of(&dom, p).y + 2 + 9,
"C inside P: P.y + padding (2) + C.mt (9)"
);
}
#[test]
fn empty_block_between_siblings_collapses_top_and_bottom_through() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let e = dom.create_element("e");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, e).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(2),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"e",
TuiStyle::new().margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(4),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(1),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, b).y,
5,
"all four margins collapse to max(2,3,4,1)=4"
);
}
#[test]
fn parent_top_margin_collapses_with_first_child_when_no_padding_or_border() {
let mut dom = dom();
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().height(Size::Fixed(10)).margin(Margin::new(
MarginValue::Cells(4),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, child).y,
0,
"child top margin collapsed through parent (no padding/border) → child.y = parent.y (= 0)"
);
}
#[test]
fn parent_top_padding_blocks_first_child_collapse() {
let mut dom = dom();
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()
.height(Size::Fixed(10))
.padding(Padding::new(2, 0, 0, 0)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
use crate::render::Rect;
dom.layout_dom(Rect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, child).y,
2 + 3,
"padding blocks collapse — child sits below padding + own margin"
);
}
#[test]
fn parent_top_border_blocks_first_child_collapse() {
use crate::layout::Border;
let mut dom = dom();
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()
.height(Size::Fixed(10))
.border(Border::single()),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
use crate::render::Rect;
dom.layout_dom(Rect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, child).y,
1 + 3,
"border blocks collapse — child sits below border row + own margin"
);
}
#[test]
fn parent_overflow_hidden_blocks_first_child_collapse() {
use crate::layout::Overflow;
let mut dom = dom();
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()
.height(Size::Fixed(10))
.overflow(Overflow::Hidden),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
use crate::render::Rect;
dom.layout_dom(Rect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, child).y,
3,
"overflow: hidden establishes new BFC → child margin applies inside"
);
}
#[test]
fn adjacent_negative_margins_collapse_to_most_negative() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(-3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(-1),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, b).y,
2 + (-3),
"A.height (2) + min(-3, -1) = -1 → B at y=-1"
);
}
#[test]
fn adjacent_mixed_sign_margins_sum_via_positive_plus_negative() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(5),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(-3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, b).y,
2 + (5 + (-3)),
"A.height (2) + (5 + -3 = 2) = 4"
);
}
#[test]
fn absolutely_positioned_sibling_does_not_break_margin_adjacency() {
use crate::layout::Position;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let abs = dom.create_element("z");
let c = dom.create_element("c");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, abs).unwrap();
dom.append_child(parent, c).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked("z", TuiStyle::new().position(Position::Absolute))
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, c).y,
1 + 3,
"C.y = A.bottom (1) + max(A.mb=3, C.mt=2) = 4 — out-of-flow Z skipped"
);
}
#[test]
fn absolutely_positioned_child_does_not_advance_cursor() {
use crate::layout::Position;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let abs = dom.create_element("z");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, abs).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(2)))
.rule_unchecked("z", TuiStyle::new().position(Position::Absolute))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, b).y, 2);
}
#[test]
fn display_none_child_does_not_advance_cursor() {
use crate::layout::Display;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let hidden = dom.create_element("z");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, hidden).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(2)))
.rule_unchecked("z", TuiStyle::new().display(Display::None))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, b).y, 2);
}
#[test]
fn padding_on_block_container_is_handled_by_layout_node() {
let mut dom = dom();
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();
cascade(&mut dom, &Stylesheet::bare());
run_block(&mut dom, parent, LayoutRect::new(3, 1, 70, 20));
let r = layout_of(&dom, child);
assert_eq!(r.x, 3, "child anchored at container.x");
assert_eq!(r.y, 1, "child anchored at container.y");
assert_eq!(r.width, 70);
}
fn anon_blocks_of(dom: &TuiDom, id: NodeId) -> Vec<crate::ext::AnonymousIfc> {
dom.node(id).ext().unwrap().anonymous_blocks.clone()
}
#[test]
fn text_only_paragraph_wraps_in_anonymous_block() {
let mut dom = dom();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello world");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
cascade(&mut dom, &Stylesheet::bare());
run_block(&mut dom, p, LayoutRect::new(0, 0, 20, 24));
let anons = anon_blocks_of(&dom, p);
assert_eq!(anons.len(), 1, "one anonymous block wraps the text");
assert_eq!(anons[0].rect.x, 0);
assert_eq!(anons[0].rect.y, 0);
assert_eq!(anons[0].rect.width, 20);
assert_eq!(
anons[0].rect.height, 1,
"11-char text fits on one line at width 20"
);
}
#[test]
fn mixed_text_and_inline_element_share_one_anonymous_block() {
use crate::layout::Display;
let mut dom = dom();
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("text ");
let em = dom.create_element("em");
let t_em = dom.create_text_node("italic");
dom.append_child(em, t_em).unwrap();
let t2 = dom.create_text_node(" more");
dom.append_child(p, t1).unwrap();
dom.append_child(p, em).unwrap();
dom.append_child(p, t2).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("em", TuiStyle::new().display(Display::Inline));
cascade(&mut dom, &sheet);
run_block(&mut dom, p, LayoutRect::new(0, 0, 30, 24));
let anons = anon_blocks_of(&dom, p);
assert_eq!(
anons.len(),
1,
"consecutive inline-level children share one anonymous block"
);
assert_eq!(anons[0].child_range, (0, 3));
}
#[test]
fn block_then_text_then_block_produces_anon_block_in_the_middle() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let h1 = dom.create_element("h1");
let h1_t = dom.create_text_node("X");
dom.append_child(h1, h1_t).unwrap();
let text = dom.create_text_node("middle text");
let h2 = dom.create_element("h2");
let h2_t = dom.create_text_node("Y");
dom.append_child(h2, h2_t).unwrap();
dom.append_child(parent, h1).unwrap();
dom.append_child(parent, text).unwrap();
dom.append_child(parent, h2).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("h1", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked("h2", TuiStyle::new().height(Size::Fixed(1)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 20, 24));
let anons = anon_blocks_of(&dom, parent);
assert_eq!(anons.len(), 1, "only the middle inline run wraps");
assert_eq!(anons[0].rect.y, 1);
assert_eq!(anons[0].rect.height, 1);
assert_eq!(anons[0].child_range, (1, 2), "wraps the text node only");
assert_eq!(layout_of(&dom, h1).y, 0);
assert_eq!(layout_of(&dom, h2).y, 2, "h2 follows anon block");
}
#[test]
fn multiple_inline_runs_separated_by_block_each_get_their_own_anonymous() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let t1 = dom.create_text_node("one");
let h1 = dom.create_element("h1");
let h1t = dom.create_text_node("X");
dom.append_child(h1, h1t).unwrap();
let t2 = dom.create_text_node("two");
let h2 = dom.create_element("h2");
let h2t = dom.create_text_node("Y");
dom.append_child(h2, h2t).unwrap();
let t3 = dom.create_text_node("three");
dom.append_child(parent, t1).unwrap();
dom.append_child(parent, h1).unwrap();
dom.append_child(parent, t2).unwrap();
dom.append_child(parent, h2).unwrap();
dom.append_child(parent, t3).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("h1", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked("h2", TuiStyle::new().height(Size::Fixed(1)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 20, 24));
let anons = anon_blocks_of(&dom, parent);
assert_eq!(anons.len(), 3, "three inline runs → three anonymous blocks");
assert_eq!(anons[0].child_range, (0, 1));
assert_eq!(anons[1].child_range, (2, 3));
assert_eq!(anons[2].child_range, (4, 5));
}
#[test]
fn pure_block_container_has_no_anonymous_boxes() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let h1 = dom.create_element("h1");
let h1t = dom.create_text_node("X");
dom.append_child(h1, h1t).unwrap();
let body = dom.create_element("body");
let bodyt = dom.create_text_node("Y");
dom.append_child(body, bodyt).unwrap();
dom.append_child(parent, h1).unwrap();
dom.append_child(parent, body).unwrap();
dom.append_child(root, parent).unwrap();
cascade(&mut dom, &Stylesheet::bare());
run_block(&mut dom, parent, LayoutRect::new(0, 0, 30, 24));
assert!(
anon_blocks_of(&dom, parent).is_empty(),
"pure-block container has no anonymous boxes"
);
}
#[test]
fn inline_block_inside_paragraph_packs_as_atomic_and_brackets_render() {
use crate::render::{Buffer, PaintExt, Rect};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("hi ");
let btn = dom.create_element("button");
let btn_t = dom.create_text_node("X");
dom.append_child(btn, btn_t).unwrap();
let t2 = dom.create_text_node(" ok");
dom.append_child(p, t1).unwrap();
dom.append_child(p, btn).unwrap();
dom.append_child(p, t2).unwrap();
dom.append_child(root, p).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.cascade(&Stylesheet::new());
let viewport = Rect::new(0, 0, 30, 5);
dom.layout_dom(viewport);
let mut buf = Buffer::empty(viewport);
dom.paint_dom(&mut buf, viewport);
let mut row = String::new();
for x in 0..30 {
if let Some(c) = buf.cell(x, 0)
&& !c.is_spacer()
{
row.push_str(c.symbol());
}
}
let i_hi = row.find("hi").expect("hi missing");
let i_btn = row.find("[ X ]").expect("button UA chrome missing");
let i_ok = row.find("ok").expect("ok missing");
assert!(
i_hi < i_btn && i_btn < i_ok,
"inline-flow order broken: got {row:?}"
);
assert!(
row.contains("hi [ X ] ok"),
"missing separator space around atomic inline-block: got {row:?}"
);
}
#[test]
fn inline_flow_for_text_resolves_anon_box() {
use crate::ext::TuiExt;
use crate::render::inline::{InlineFlow, inline_flow_for_text, inline_flow_layout};
use crate::render::layout_pass::block::layout_block_children;
use rdom_core::Dom;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let h1 = dom.create_element("h1");
let h1t = dom.create_text_node("X");
dom.append_child(h1, h1t).unwrap();
let text = dom.create_text_node("middle");
dom.append_child(parent, h1).unwrap();
dom.append_child(parent, text).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("h1", TuiStyle::new().height(Size::Fixed(1)));
dom.cascade(&sheet);
let parent_rect = LayoutRect::new(0, 0, 20, 10);
if let Some(ext) = dom.node_mut(parent).ext_mut() {
ext.layout = parent_rect;
ext.content_layout = parent_rect;
}
let parent_computed = dom.node(parent).computed().cloned().unwrap_or_default();
layout_block_children(
&mut dom as &mut Dom<TuiExt>,
parent,
parent_rect,
&parent_computed,
);
let flow = inline_flow_for_text(&dom, text).expect("text resolves to a flow");
assert!(
matches!(flow, InlineFlow::Anonymous { container, .. } if container == parent),
"text in anon box resolves to InlineFlow::Anonymous on the parent, got {flow:?}",
);
let (layout, rect) = inline_flow_layout(&dom, flow).expect("anon flow has layout + rect");
assert!(!layout.lines.is_empty(), "anon IFC has at least one line");
assert_eq!(rect.y, 1, "anon box at y=1 (below h1)");
}
#[test]
fn anonymous_block_hit_test_resolves_to_source_text_node() {
use crate::ext::TuiExt;
use crate::render::layout_pass::block::layout_block_children;
use crate::render::{PaintExt, Rect};
use crate::runtime::hit_test::HitTestExt;
use rdom_core::Dom;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let h1 = dom.create_element("h1");
let h1t = dom.create_text_node("X");
dom.append_child(h1, h1t).unwrap();
let text = dom.create_text_node("middle");
let h2 = dom.create_element("h2");
let h2t = dom.create_text_node("Y");
dom.append_child(h2, h2t).unwrap();
dom.append_child(parent, h1).unwrap();
dom.append_child(parent, text).unwrap();
dom.append_child(parent, h2).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("h1", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked("h2", TuiStyle::new().height(Size::Fixed(1)));
dom.cascade(&sheet);
let parent_rect = LayoutRect::new(0, 0, 20, 10);
if let Some(ext) = dom.node_mut(parent).ext_mut() {
ext.layout = parent_rect;
ext.content_layout = parent_rect;
}
if let Some(ext) = dom.node_mut(root).ext_mut() {
ext.layout = parent_rect;
ext.content_layout = parent_rect;
}
let parent_computed = dom.node(parent).computed().cloned().unwrap_or_default();
layout_block_children(
&mut dom as &mut Dom<TuiExt>,
parent,
parent_rect,
&parent_computed,
);
let mut buf = crate::render::Buffer::empty(Rect::new(0, 0, 20, 10));
dom.paint_dom(&mut buf, Rect::new(0, 0, 20, 10));
let pos = dom
.position_at(2, 1)
.expect("anon-box click resolves to a position");
assert_eq!(pos.node, text, "click routes to the middle text node");
assert_eq!(pos.offset, 2, "byte offset within the text node");
}
#[test]
fn anonymous_block_text_paints_at_anon_block_rect() {
use crate::ext::TuiExt;
use crate::render::layout_pass::block::layout_block_children;
use crate::render::{Buffer, PaintExt, Rect};
use rdom_core::Dom;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let h1 = dom.create_element("h1");
let h1t = dom.create_text_node("X");
dom.append_child(h1, h1t).unwrap();
let text = dom.create_text_node("middle");
let h2 = dom.create_element("h2");
let h2t = dom.create_text_node("Y");
dom.append_child(h2, h2t).unwrap();
dom.append_child(parent, h1).unwrap();
dom.append_child(parent, text).unwrap();
dom.append_child(parent, h2).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("h1", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked("h2", TuiStyle::new().height(Size::Fixed(1)));
dom.cascade(&sheet);
let parent_rect = LayoutRect::new(0, 0, 20, 10);
if let Some(ext) = dom.node_mut(parent).ext_mut() {
ext.layout = parent_rect;
ext.content_layout = parent_rect;
}
let parent_computed = dom.node(parent).computed().cloned().unwrap_or_default();
layout_block_children(
&mut dom as &mut Dom<TuiExt>,
parent,
parent_rect,
&parent_computed,
);
let viewport = Rect::new(0, 0, 20, 10);
let mut buf = Buffer::empty(viewport);
dom.paint_dom(&mut buf, viewport);
let mut row1 = String::new();
for x in 0..20 {
if let Some(c) = buf.cell(x, 1)
&& !c.is_spacer()
{
row1.push_str(c.symbol());
}
}
assert!(
row1.contains("middle"),
"anon-box text should paint at row 1; got {row1:?}"
);
}
#[test]
fn out_of_flow_children_do_not_break_inline_runs() {
use crate::layout::Position;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let t1 = dom.create_text_node("one ");
let abs = dom.create_element("abs");
let t2 = dom.create_text_node("two");
dom.append_child(parent, t1).unwrap();
dom.append_child(parent, abs).unwrap();
dom.append_child(parent, t2).unwrap();
dom.append_child(root, parent).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("abs", TuiStyle::new().position(Position::Absolute));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 30, 24));
let anons = anon_blocks_of(&dom, parent);
assert_eq!(
anons.len(),
1,
"two text nodes adjacent in flow share one anon block"
);
}
#[test]
fn child_with_padding_and_border_reduces_intrinsic_width_via_layout_node() {
let mut dom = dom();
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(
"c",
TuiStyle::new()
.padding(Padding {
top: PaddingValue::Cells(0),
right: PaddingValue::Cells(2),
bottom: PaddingValue::Cells(0),
left: PaddingValue::Cells(2),
})
.border(Border::single()),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, child).width, 80);
}
#[test]
fn auto_height_block_sums_two_fixed_children() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(3)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(5)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 50));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
8,
"auto height = sum of children = 3 + 5"
);
}
#[test]
fn auto_height_block_includes_collapsed_inter_sibling_margin() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(4)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 50));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
2 + 3 + 4,
"auto height includes collapsed inter-sibling margin gap"
);
}
#[test]
fn block_height_fixed_overflows_excess_children() {
use crate::render::Rect;
let mut dom = dom();
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().height(Size::Fixed(4)))
.rule_unchecked("c", TuiStyle::new().height(Size::Fixed(10)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
4,
"fixed-height parent stays 4 even with 10-tall child"
);
assert_eq!(dom.node(child).ext().unwrap().layout.height, 10);
}
#[test]
fn min_height_floors_block_above_content() {
use crate::layout::MinSize;
use crate::render::Rect;
let mut dom = dom();
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().min_height(MinSize::Cells(8)))
.rule_unchecked("c", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
8,
"parent floored to min-height even though content is only 3"
);
}
#[test]
fn max_height_caps_block_below_content() {
use crate::render::Rect;
let mut dom = dom();
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().max_height(5))
.rule_unchecked("c", TuiStyle::new().height(Size::Fixed(12)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
5,
"parent capped at max-height even though content is 12"
);
}
#[test]
fn min_height_keeps_empty_block_open_blocking_collapse_through() {
use crate::layout::MinSize;
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let e = dom.create_element("e");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, e).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(4),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"e",
TuiStyle::new()
.min_height(MinSize::Cells(2))
.margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(5),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(layout_of(&dom, b).y, 12);
}
#[test]
fn percent_height_resolves_against_definite_parent() {
use crate::render::Rect;
let mut dom = dom();
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().height(Size::Fixed(20)))
.rule_unchecked("c", TuiStyle::new().height(Size::Percent(50)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(
dom.node(child).ext().unwrap().layout.height,
10,
"50% of definite parent height (20) = 10"
);
}
#[test]
fn percent_height_falls_to_auto_when_parent_height_is_indefinite() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let child = dom.create_element("c");
let gc = dom.create_element("g");
dom.append_child(child, gc).unwrap();
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("c", TuiStyle::new().height(Size::Percent(50)))
.rule_unchecked("g", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(
dom.node(child).ext().unwrap().layout.height,
3,
"indefinite parent → percent height falls to auto (intrinsic = 3)"
);
}
#[test]
fn auto_height_block_with_overflow_hidden_includes_descendant_margins() {
use crate::layout::Overflow;
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
dom.append_child(parent, a).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("p", TuiStyle::new().overflow(Overflow::Hidden))
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 50));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
3 + 2,
"BFC parent traps child's top margin → height includes it"
);
}
#[test]
fn vertical_auto_margin_resolves_to_zero_not_distribution() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Auto,
MarginValue::Cells(0),
MarginValue::Auto,
MarginValue::Cells(0),
)),
)
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, a).y, 0);
assert_eq!(layout_of(&dom, b).y, 2);
}
#[test]
fn anonymous_block_box_does_not_collapse_margins_with_adjacent_block() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let txt = dom.create_text_node("inline text");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, txt).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(5),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(3),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, b).y,
10,
"anon-block boundary prevents A.mb from collapsing with B.mt — anon takes A.mb on its top, B.mt applies fresh"
);
}
#[test]
fn parent_bottom_margin_collapses_with_last_child() {
let mut dom = dom();
let root = dom.root();
let gp = dom.create_element("gp");
let p = dom.create_element("p");
let c = dom.create_element("c");
let next = dom.create_element("n");
dom.append_child(p, c).unwrap();
dom.append_child(gp, p).unwrap();
dom.append_child(gp, next).unwrap();
dom.append_child(root, gp).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"c",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(7),
MarginValue::Cells(0),
)),
)
.rule_unchecked("n", TuiStyle::new().height(Size::Fixed(1)));
cascade(&mut dom, &sheet);
run_block(&mut dom, gp, LayoutRect::new(0, 0, 80, 24));
assert_eq!(
layout_of(&dom, next).y,
2 + 7,
"N.y = max(P.mb=3, C.mb=7) above gp.y_cursor after P"
);
}
#[test]
fn display_none_sibling_does_not_break_margin_adjacency() {
use crate::layout::Display;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let h = dom.create_element("h");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, h).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(4),
MarginValue::Cells(0),
)),
)
.rule_unchecked("h", TuiStyle::new().display(Display::None))
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(1)).margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, b).y, 1 + 4);
}
#[test]
fn negative_top_with_positive_bottom_partially_cancels() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(5),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(-2),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, b).y, 2 + (5 - 2));
}
#[test]
fn parent_with_only_empty_collapse_through_children_has_zero_content() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let e1 = dom.create_element("e1");
let e2 = dom.create_element("e2");
dom.append_child(parent, e1).unwrap();
dom.append_child(parent, e2).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"e1",
TuiStyle::new().margin(Margin::new(
MarginValue::Cells(2),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"e2",
TuiStyle::new().margin(Margin::new(
MarginValue::Cells(1),
MarginValue::Cells(0),
MarginValue::Cells(4),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
assert_eq!(
dom.node(parent).ext().unwrap().layout.height,
0,
"all-empty parent collapses to 0 content height"
);
}
#[test]
fn min_width_floors_under_auto_margin_distribution() {
use crate::layout::MinSize;
let mut dom = dom();
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(
"c",
TuiStyle::new()
.width(Size::Fixed(10))
.min_width(MinSize::Cells(60))
.margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Auto,
MarginValue::Cells(0),
MarginValue::Auto,
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, child).width, 60, "min-width clamp wins");
}
#[test]
fn max_width_caps_before_auto_margins() {
let mut dom = dom();
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(
"c",
TuiStyle::new().max_width(30).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Auto,
MarginValue::Cells(0),
MarginValue::Auto,
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
let r = layout_of(&dom, child);
assert_eq!(r.width, 30, "max-width caps");
assert_eq!(r.x, 25, "leftover (50) splits 25/25");
}
#[test]
fn width_percent_resolves_against_definite_containing_block() {
let mut dom = dom();
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(40)))
.rule_unchecked("c", TuiStyle::new().width(Size::Percent(25)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 40, 24));
assert_eq!(layout_of(&dom, child).width, 10, "25% of 40 = 10");
}
#[test]
fn percent_height_chains_through_three_definite_levels() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let gp = dom.create_element("gp");
let p = dom.create_element("p");
let c = dom.create_element("c");
dom.append_child(p, c).unwrap();
dom.append_child(gp, p).unwrap();
dom.append_child(root, gp).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("gp", TuiStyle::new().height(Size::Fixed(40)))
.rule_unchecked("p", TuiStyle::new().height(Size::Percent(50)))
.rule_unchecked("c", TuiStyle::new().height(Size::Percent(50)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 60));
assert_eq!(dom.node(p).ext().unwrap().layout.height, 20);
assert_eq!(dom.node(c).ext().unwrap().layout.height, 10);
}
#[test]
fn percent_height_breaks_chain_at_auto_ancestor() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let gp = dom.create_element("gp");
let p = dom.create_element("p");
let c = dom.create_element("c");
let inner = dom.create_element("i");
dom.append_child(c, inner).unwrap();
dom.append_child(p, c).unwrap();
dom.append_child(gp, p).unwrap();
dom.append_child(root, gp).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("gp", TuiStyle::new().height(Size::Fixed(40)))
.rule_unchecked("c", TuiStyle::new().height(Size::Percent(50)))
.rule_unchecked("i", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 60));
assert_eq!(
dom.node(c).ext().unwrap().layout.height,
3,
"percent height falls to intrinsic (inner fixed 3) because parent P is Auto"
);
}
#[test]
fn block_inside_flex_inside_block() {
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let outer = dom.create_element("outer");
let middle = dom.create_element("middle");
let inner = dom.create_element("inner");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(inner, a).unwrap();
dom.append_child(inner, b).unwrap();
dom.append_child(middle, inner).unwrap();
dom.append_child(outer, middle).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"middle",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.height(Size::Fixed(20)),
)
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(5)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(7)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 60));
let a_rect = dom.node(a).ext().unwrap().layout;
let b_rect = dom.node(b).ext().unwrap().layout;
assert_eq!(a_rect.y, 0, "a at inner's content top");
assert_eq!(b_rect.y, 5, "b stacks below a");
assert_eq!(a_rect.height, 5);
assert_eq!(b_rect.height, 7);
}
#[test]
fn relative_positioned_child_does_not_shift_subsequent_block_siblings() {
use crate::layout::{Length, Position};
use crate::render::Rect;
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new()
.height(Size::Fixed(3))
.position(Position::Relative)
.top(Length::Cells(5)),
)
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(2)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 50));
assert_eq!(
dom.node(b).ext().unwrap().layout.y,
3,
"b's position is independent of A's relative shift"
);
}
#[test]
fn block_gap_adds_to_collapsed_margins() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("p", TuiStyle::new().gap(2))
.rule_unchecked(
"a",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(3),
MarginValue::Cells(0),
)),
)
.rule_unchecked(
"b",
TuiStyle::new().height(Size::Fixed(2)).margin(Margin::new(
MarginValue::Cells(1),
MarginValue::Cells(0),
MarginValue::Cells(0),
MarginValue::Cells(0),
)),
);
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, b).y, 2 + 3 + 2);
}
#[test]
fn block_gap_not_applied_before_first_or_after_last() {
let mut dom = dom();
let root = dom.root();
let parent = dom.create_element("p");
let a = dom.create_element("a");
dom.append_child(parent, a).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("p", TuiStyle::new().gap(5))
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(2)));
cascade(&mut dom, &sheet);
run_block(&mut dom, parent, LayoutRect::new(0, 0, 80, 24));
assert_eq!(layout_of(&dom, a).y, 0);
}
#[test]
fn mixed_content_block_advances_sibling_past_full_height() {
let mut dom = dom();
let root = dom.root();
let outer = dom.create_element("div");
dom.append_child(root, outer).unwrap();
let first = dom.create_element("div");
let ft = dom.create_text_node("FIRST");
dom.append_child(first, ft).unwrap();
let inner = dom.create_element("div");
let it = dom.create_text_node("INNER");
dom.append_child(inner, it).unwrap();
dom.append_child(first, inner).unwrap();
dom.append_child(outer, first).unwrap();
let second = dom.create_element("div");
let st = dom.create_text_node("SECOND");
dom.append_child(second, st).unwrap();
dom.append_child(outer, second).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().display(Display::Block));
dom.cascade(&sheet);
dom.layout_dom(Rect::new(0, 0, 20, 6));
assert_eq!(layout_of(&dom, first).height, 2, "first spans text + inner");
assert_eq!(layout_of(&dom, inner).y, 1, "inner sits below the text row");
assert_eq!(
layout_of(&dom, second).y,
2,
"sibling must clear first's full height, not overlap inner"
);
}