use super::*;
use crate::TuiDom;
use crate::layout::{Border, Direction, Display, Flow, Overflow, Padding, Size};
use crate::render::{LayoutExt, Rect};
use crate::style::{CascadeExt, Color, Stylesheet, TuiStyle};
use rdom_core::NodeId;
fn prepare(dom: &mut TuiDom, sheet: &Stylesheet, viewport: Rect) {
dom.cascade(sheet);
dom.layout_dom(viewport);
}
#[test]
fn point_on_single_block_returns_it() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(5, 1), Some(div));
}
#[test]
fn point_outside_viewport_returns_none() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(2)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(50, 50), None);
}
#[test]
fn empty_tree_returns_none() {
let mut dom: TuiDom = TuiDom::new();
prepare(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(5, 5), None);
}
#[test]
fn deepest_block_wins_on_nesting() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("div");
let inner = dom.create_element("span");
dom.append_child(outer, inner).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.padding(Padding::all(1)),
)
.rule_unchecked(
"span",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(3, 1), Some(inner));
}
#[test]
fn paint_order_stacking_later_sibling_wins() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
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(
"div",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.gap(0)
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 1), Some(a));
assert_eq!(dom.hit_test(7, 1), Some(b));
dom.node_mut(parent).ext_mut().unwrap().scroll_x = 0;
dom.layout_dom(Rect::new(0, 0, 20, 10)); let overlap = dom.node(a).layout_rect().unwrap();
dom.node_mut(b).ext_mut().unwrap().layout = overlap;
assert_eq!(dom.hit_test(2, 1), Some(b));
}
#[test]
fn overflow_hidden_clips_children_hit_area() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.padding(Padding::all(1))
.overflow(Overflow::Hidden),
)
.rule_unchecked(
"span",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(0, 0), Some(parent));
assert_eq!(dom.hit_test(2, 1), Some(child));
}
#[test]
fn overflow_visible_allows_child_hit_past_parent_rect() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
)
.rule_unchecked(
"span",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 30, 10));
let mut rect = dom.node(child).layout_rect().unwrap();
rect.x = 3;
rect.width = 8; dom.node_mut(child).ext_mut().unwrap().layout = rect;
assert_eq!(dom.hit_test(1, 1), Some(parent));
assert_eq!(dom.hit_test(4, 1), Some(child));
assert_eq!(dom.hit_test(9, 1), None);
}
#[test]
fn ifc_block_returns_fragment_owner() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("ab ");
let code = dom.create_element("code");
let ct = dom.create_text_node("X");
dom.append_child(code, ct).unwrap();
dom.append_child(p, t1).unwrap();
dom.append_child(p, code).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(20))
.height(Size::Fixed(1)),
)
.rule_unchecked("code", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
let path = dom.hit_test_path(0, 0);
assert_eq!(path.last().copied(), Some(p));
let path = dom.hit_test_path(3, 0);
assert_eq!(path.last().copied(), Some(code));
assert!(
path.contains(&p),
"path must include p (IFC block) as ancestor"
);
}
#[test]
fn ifc_click_in_padding_returns_ifc_block_not_fragment() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("X");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap(); dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.padding(Padding::all(1))
.overflow(Overflow::Hidden),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(0, 0), Some(p));
}
#[test]
fn ifc_wrapped_inline_hittable_on_any_line() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let b = dom.create_element("b");
let bt = dom.create_text_node("aaa bbb ccc");
dom.append_child(b, bt).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(4)),
)
.rule_unchecked("b", TuiStyle::new().display(Display::Inline).bold(true));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
for y in 0..3 {
let path = dom.hit_test_path(0, y);
let last = path.last().copied();
assert_eq!(last, Some(b), "wrapped <b> must be hittable on line {y}");
assert!(path.contains(&p));
}
}
#[test]
fn path_includes_full_ancestor_chain() {
let mut dom: TuiDom = 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(b, c).unwrap();
dom.append_child(a, b).unwrap();
dom.append_child(root, a).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5)),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(8)).height(Size::Fixed(3)),
)
.rule_unchecked(
"c",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
let path = dom.hit_test_path(1, 0);
assert_eq!(path, vec![a, b, c]);
}
#[test]
fn path_empty_when_nothing_hit() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(3)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test_path(50, 50), Vec::<NodeId>::new());
}
#[test]
fn reverse_document_order_descent_picks_last_sibling() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let first = dom.create_element("first");
let second = dom.create_element("second");
dom.append_child(root, first).unwrap();
dom.append_child(root, second).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"first",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(2)),
)
.rule_unchecked(
"second",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(2)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
let r = dom.node(first).layout_rect().unwrap();
dom.node_mut(second).ext_mut().unwrap().layout = r;
assert_eq!(dom.hit_test(2, 1), Some(second));
}
#[test]
fn hit_on_border_returns_the_element() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(4))
.border(Border::single())
.fg(Color::Rgb(255, 255, 255)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(0, 0), Some(div));
}
use crate::layout::UserSelect;
use rdom_core::Position;
#[test]
fn position_at_outside_viewport_returns_none() {
let mut dom: TuiDom = TuiDom::new();
prepare(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(99, 99), None);
}
#[test]
fn position_at_inside_text_fragment_returns_source_position() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t, 2)));
assert_eq!(dom.position_at(4, 0), Some(Position::new(t, 4)));
}
#[test]
fn position_at_across_inline_element_boundary_uses_text_node_of_fragment() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("ab");
dom.append_child(p, t1).unwrap();
let code = dom.create_element("code");
let t2 = dom.create_text_node("XY");
dom.append_child(code, t2).unwrap();
dom.append_child(p, code).unwrap();
let t3 = dom.create_text_node("cd");
dom.append_child(p, t3).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("code", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t1, 0)));
assert_eq!(dom.position_at(1, 0), Some(Position::new(t1, 1)));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t2, 0)));
assert_eq!(dom.position_at(3, 0), Some(Position::new(t2, 1)));
assert_eq!(dom.position_at(4, 0), Some(Position::new(t3, 0)));
assert_eq!(dom.position_at(5, 0), Some(Position::new(t3, 1)));
}
#[test]
fn position_at_on_cjk_grapheme_snaps_to_start() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("中文");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(1, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t, 3)));
assert_eq!(dom.position_at(3, 0), Some(Position::new(t, 3)));
}
#[test]
fn position_at_respects_user_select_none() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hi");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.user_select(UserSelect::None),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), None);
assert_eq!(dom.position_at(1, 0), None);
}
#[test]
fn position_at_user_select_none_inherits_to_subtree() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let wrapper = dom.create_element("wrapper");
let p = dom.create_element("p");
let t = dom.create_text_node("hi");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(wrapper, p).unwrap();
dom.append_child(root, wrapper).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("wrapper", TuiStyle::new().user_select(UserSelect::None))
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), None);
}
#[test]
fn higher_z_index_catches_click_first() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let hi = dom.create_element("hi");
let lo = dom.create_element("lo");
dom.append_child(root, hi).unwrap();
dom.append_child(root, lo).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(5))
.height(Size::Fixed(2))
};
let sheet = Stylesheet::bare()
.rule_unchecked("hi", base().z_index(crate::layout::ZIndex::Value(5)))
.rule_unchecked("lo", base().z_index(crate::layout::ZIndex::Value(1)));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 0), Some(hi));
}
#[test]
fn positioned_catches_click_over_in_flow_content() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let tip = dom.create_element("tip");
let bar = dom.create_element("bar");
dom.append_child(root, tip).unwrap();
dom.append_child(root, bar).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"bar",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(3)),
)
.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(5))
.height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 0), Some(tip));
assert_eq!(dom.hit_test(10, 0), Some(bar));
}
#[test]
fn z_list_among_positioned_uses_doc_order_for_auto() {
let mut dom: TuiDom = 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(5))
.height(Size::Fixed(2))
};
let sheet = Stylesheet::bare()
.rule_unchecked("a", base())
.rule_unchecked("b", base());
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 0), Some(b));
}
#[test]
fn click_outside_positioned_falls_through_to_in_flow() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let tip = dom.create_element("tip");
let bar = dom.create_element("bar");
dom.append_child(root, tip).unwrap();
dom.append_child(root, bar).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"bar",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(3)),
)
.rule_unchecked(
"tip",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(2))
.left(crate::layout::Length::Cells(10))
.width(Size::Fixed(3))
.height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(5, 1), Some(bar));
}
#[test]
fn position_at_in_non_ifc_returns_none() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(3, 1), None);
}