use super::*;
use crate::layout::{Border, Direction, Display, Flow, Overflow, Padding, Size, WhiteSpace};
use crate::style::{Color, Content, Modifier, Stylesheet, TuiStyle};
use crate::{TuiDom, TuiNodeMutExt};
use rdom_core::NodeId;
fn dom_with_div() -> (TuiDom, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
(dom, div)
}
#[test]
fn fg_inherits_to_child() {
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().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).fg, Color::Rgb(255, 0, 0));
assert_eq!(computed_of(&dom, child).fg, Color::Rgb(255, 0, 0)); }
#[test]
fn bg_does_not_inherit() {
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().bg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).bg, Color::Rgb(255, 0, 0));
assert_eq!(computed_of(&dom, child).bg, Color::Reset); }
#[test]
fn bold_inherits() {
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().bold(true));
dom.cascade(&sheet);
assert!(computed_of(&dom, parent).modifiers.contains(Modifier::BOLD));
assert!(computed_of(&dom, child).modifiers.contains(Modifier::BOLD));
}
#[test]
fn higher_specificity_wins() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).set_id("hero").unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked("#hero", TuiStyle::new().fg(Color::Rgb(0, 0, 255)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 0, 255));
}
#[test]
fn same_specificity_source_order_wins() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).add_class("a").unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(".a", TuiStyle::new().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked(".a", TuiStyle::new().fg(Color::Rgb(0, 0, 255)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 0, 255));
}
#[test]
fn inline_beats_stylesheet_rule() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div)
.set_inline_style(TuiStyle::new().fg(Color::Rgb(0, 128, 0)));
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 128, 0));
}
#[test]
fn inline_beats_id_rule_too() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).set_id("x").unwrap();
dom.node_mut(div)
.set_inline_style(TuiStyle::new().fg(Color::Rgb(0, 128, 0)));
let sheet = Stylesheet::bare().rule_unchecked("#x", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 128, 0));
}
#[test]
fn important_rule_beats_normal_inline() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div)
.set_inline_style(TuiStyle::new().fg(Color::Rgb(0, 128, 0)));
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().fg_important(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn important_inline_beats_important_author() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div)
.set_inline_style(TuiStyle::new().fg_important(Color::Rgb(0, 128, 0)));
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().fg_important(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn ua_rule_loses_to_author() {
let (mut dom, div) = dom_with_div();
dom.set_attribute(div, "disabled", "").unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(127, 134, 139));
let sheet =
Stylesheet::new().rule_unchecked("[disabled]", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn ua_important_beats_author_important() {
let (mut dom, div) = dom_with_div();
dom.set_attribute(div, "disabled", "").unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"[disabled]",
TuiStyle::new().fg_important(Color::Rgb(255, 0, 0)),
)
.rule_unchecked(
"[disabled]",
TuiStyle::new().fg_important(Color::Rgb(0, 0, 255)),
);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 0, 255));
}
#[test]
fn fg_initial_restores_spec_default() {
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().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked("span", TuiStyle::new().fg_initial());
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).fg, Color::Rgb(255, 0, 0));
assert_eq!(computed_of(&dom, child).fg, Color::Reset);
}
#[test]
fn fg_inherit_keyword_forces_parent_value() {
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().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked("span", TuiStyle::new().fg_inherit());
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn before_pseudo_created_from_rule() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div::before",
TuiStyle::new().content(Content::Str("▾".into())),
);
dom.cascade(&sheet);
let ext = dom.node(div).ext().unwrap();
let before = ext.computed_before.as_ref().unwrap();
assert_eq!(before.content.as_deref(), Some("▾"));
}
#[test]
fn pseudo_position_cascades_through_to_computed_style() {
use crate::layout::{Length, Position};
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div::before",
TuiStyle::new()
.content(Content::Str("[".into()))
.position(Position::Absolute)
.left(Length::Cells(0)),
)
.rule_unchecked(
"div::after",
TuiStyle::new()
.content(Content::Str("]".into()))
.position(Position::Absolute)
.right(Length::Cells(0)),
);
dom.cascade(&sheet);
let ext = dom.node(div).ext().unwrap();
let before = ext.computed_before.as_ref().expect("::before computed");
assert_eq!(before.position, Position::Absolute);
assert_eq!(before.left, Length::Cells(0));
let after = ext.computed_after.as_ref().expect("::after computed");
assert_eq!(after.position, Position::Absolute);
assert_eq!(after.right, Length::Cells(0));
}
#[test]
fn after_pseudo_created_from_rule() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div::after",
TuiStyle::new().content(Content::Str("→".into())),
);
dom.cascade(&sheet);
let ext = dom.node(div).ext().unwrap();
let after = ext.computed_after.as_ref().unwrap();
assert_eq!(after.content.as_deref(), Some("→"));
}
#[test]
fn pseudo_inherits_from_host_not_parent() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("section");
let host = dom.create_element("div");
dom.append_child(parent, host).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("section", TuiStyle::new().fg(Color::Rgb(0, 0, 255)))
.rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked(
"div::before",
TuiStyle::new().content(Content::Str("x".into())),
);
dom.cascade(&sheet);
let before = dom
.node(host)
.ext()
.unwrap()
.computed_before
.as_ref()
.unwrap();
assert_eq!(before.fg, Color::Rgb(255, 0, 0));
}
#[test]
fn pseudo_without_rules_or_fallback_is_none() {
let (mut dom, _div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
let ext = dom.node(_div).ext().unwrap();
assert!(ext.computed_before.is_none());
assert!(ext.computed_after.is_none());
assert!(ext.computed_selection.is_none());
}
#[test]
fn selection_pseudo_created_from_rule() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div::selection",
TuiStyle::new()
.bg(Color::Rgb(255, 255, 0))
.fg(Color::Rgb(0, 0, 0)),
);
dom.cascade(&sheet);
let ext = dom.node(div).ext().unwrap();
let sel = ext.computed_selection.as_ref().unwrap();
assert_eq!(sel.bg, Color::Rgb(255, 255, 0));
assert_eq!(sel.fg, Color::Rgb(0, 0, 0));
}
#[test]
fn selection_pseudo_inherits_from_host_not_parent() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("section");
let host = dom.create_element("div");
dom.append_child(parent, host).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("section", TuiStyle::new().fg(Color::Rgb(0, 0, 255)))
.rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked(
"div::selection",
TuiStyle::new().bg(Color::Rgb(255, 255, 0)),
);
dom.cascade(&sheet);
let sel = dom
.node(host)
.ext()
.unwrap()
.computed_selection
.as_ref()
.unwrap();
assert_eq!(sel.bg, Color::Rgb(255, 255, 0));
assert_eq!(sel.fg, Color::Rgb(255, 0, 0));
}
#[test]
fn pseudo_with_legacy_before_content_renders() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).set_before_content("▸ ");
dom.cascade(&Stylesheet::bare());
let ext = dom.node(div).ext().unwrap();
let before = ext.computed_before.as_ref().unwrap();
assert_eq!(before.content.as_deref(), Some("▸ "));
}
#[test]
fn content_rule_beats_legacy_before_content() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).set_before_content("legacy");
let sheet = Stylesheet::bare().rule_unchecked(
"div::before",
TuiStyle::new().content(Content::Str("new".into())),
);
dom.cascade(&sheet);
let before = dom
.node(div)
.ext()
.unwrap()
.computed_before
.as_ref()
.unwrap();
assert_eq!(before.content.as_deref(), Some("new"));
}
#[test]
fn content_concat_resolves() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div::before",
TuiStyle::new().content(Content::Concat(vec![
Content::Str("▾ ".into()),
Content::Str("ITEM".into()),
])),
);
dom.cascade(&sheet);
let before = dom
.node(div)
.ext()
.unwrap()
.computed_before
.as_ref()
.unwrap();
assert_eq!(before.content.as_deref(), Some("▾ ITEM"));
}
#[test]
fn content_none_suppresses_pseudo() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).set_before_content("legacy");
let sheet =
Stylesheet::bare().rule_unchecked("div::before", TuiStyle::new().content(Content::None));
dom.cascade(&sheet);
let ext = dom.node(div).ext().unwrap();
let before = ext.computed_before.as_ref().unwrap();
assert!(before.content.is_none());
}
#[test]
fn hover_pseudo_affects_cascade() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked("div:hover", TuiStyle::new().fg(Color::Rgb(0, 0, 255)));
dom.set_hovered(None);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
dom.set_hovered(Some(div));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 0, 255));
}
#[test]
fn focus_pseudo_affects_cascade() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked("div:focus", TuiStyle::new().fg(Color::Rgb(0, 128, 0)));
dom.set_focused(Some(div));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 128, 0));
}
#[test]
fn ua_disabled_rule_mutes_by_default() {
let (mut dom, div) = dom_with_div();
dom.set_attribute(div, "disabled", "").unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(127, 134, 139));
}
#[test]
fn ua_disabled_does_not_apply_to_nondisabled() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).fg, Color::Reset);
}
#[test]
fn first_cascade_marks_layout_dirty() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
assert!(dom.node(div).ext().unwrap().layout_dirty);
}
#[test]
fn paint_only_change_does_not_set_layout_dirty() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
dom.node_mut(div).ext_mut().unwrap().layout_dirty = false;
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
assert!(!dom.node(div).ext().unwrap().layout_dirty);
}
#[test]
fn layout_change_sets_layout_dirty() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
dom.node_mut(div).ext_mut().unwrap().layout_dirty = false;
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().padding(Padding::all(2)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).padding, Padding::all(2));
assert!(dom.node(div).ext().unwrap().layout_dirty);
}
#[test]
fn cascade_clears_style_dirty() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div).ext_mut().unwrap().style_dirty = true;
dom.cascade(&Stylesheet::bare());
assert!(!dom.node(div).ext().unwrap().style_dirty);
}
#[test]
fn bold_and_italic_both_set() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().bold(true).italic(true));
dom.cascade(&sheet);
let m = computed_of(&dom, div).modifiers;
assert!(m.contains(Modifier::BOLD));
assert!(m.contains(Modifier::ITALIC));
}
#[test]
fn child_can_turn_off_inherited_bold() {
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().bold(true))
.rule_unchecked("span", TuiStyle::new().bold(false));
dom.cascade(&sheet);
assert!(computed_of(&dom, parent).modifiers.contains(Modifier::BOLD));
assert!(!computed_of(&dom, child).modifiers.contains(Modifier::BOLD));
}
#[test]
fn border_fg_initial_tracks_fg() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).border_fg, Color::Rgb(255, 0, 0));
}
#[test]
fn border_fg_explicit_overrides_fg() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.fg(Color::Rgb(255, 0, 0))
.border_fg(Color::Rgb(0, 0, 255)),
);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
assert_eq!(computed_of(&dom, div).border_fg, Color::Rgb(0, 0, 255));
}
#[test]
fn padding_and_gap_cascade() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().padding(Padding::symmetric(2, 1)).gap(3),
);
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert_eq!(c.padding, Padding::symmetric(2, 1));
assert_eq!(c.gap, 3);
}
#[test]
fn width_and_height_cascade() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(40)).height(Size::Flex(1)),
);
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert_eq!(c.width, Size::Fixed(40));
assert_eq!(c.height, Size::Flex(1));
}
#[test]
fn min_max_option_cascade() {
let (mut dom, div) = dom_with_div();
let sheet =
Stylesheet::bare().rule_unchecked("div", TuiStyle::new().min_width(10).max_width(100));
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert_eq!(c.min_width, Some(rdom_style::layout::MinSize::Cells(10)));
assert_eq!(c.max_width, Some(100));
}
#[test]
fn border_collapse_does_not_inherit_to_children() {
use rdom_style::layout::BorderCollapse;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("outer");
let inner = dom.create_element("inner");
dom.append_child(outer, inner).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"outer",
TuiStyle::new().border_collapse(BorderCollapse::Collapse),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, outer).border_collapse,
BorderCollapse::Collapse,
"outer declared collapse explicitly"
);
assert_eq!(
computed_of(&dom, inner).border_collapse,
BorderCollapse::Separate,
"inner does NOT inherit outer's collapse (BORDER-MODEL-1 divergence)"
);
}
#[test]
fn border_collapse_default_is_separate() {
use rdom_style::layout::BorderCollapse;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let el = dom.create_element("el");
dom.append_child(root, el).unwrap();
let sheet = Stylesheet::bare();
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, el).border_collapse,
BorderCollapse::Separate
);
}
#[test]
fn border_direction_overflow_cascade() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.border(Border::single())
.flow(Flow::Flex)
.direction(Direction::Row)
.overflow(Overflow::Hidden),
);
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert_eq!(c.border, Border::single());
assert_eq!(c.direction, Direction::Row);
assert_eq!(c.overflow_x, Overflow::Hidden);
assert_eq!(c.overflow_y, Overflow::Hidden);
}
#[test]
fn text_node_is_skipped_but_siblings_cascade() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let text = dom.create_text_node("hi");
let span = dom.create_element("span");
dom.append_child(root, text).unwrap();
dom.append_child(root, span).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("span", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, span).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn selector_list_rule_matches_multiple_tags() {
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 sheet =
Stylesheet::bare().rule_unchecked("a, b", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, a).fg, Color::Rgb(255, 0, 0));
assert_eq!(computed_of(&dom, b).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn descendant_combinator_cascade() {
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 span", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, inner).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn cascade_subtrees_noop_on_empty_list() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
let before = computed_of(&dom, div);
dom.cascade_subtrees(&Stylesheet::bare(), &[]);
assert_eq!(computed_of(&dom, div), before);
}
#[test]
fn cascade_subtrees_inherits_from_parent_computed() {
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().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).fg, Color::Rgb(255, 0, 0));
dom.cascade_subtrees(&sheet, &[child]);
assert_eq!(computed_of(&dom, child).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn cascade_subtrees_picks_up_new_rule() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
assert_eq!(computed_of(&dom, div).fg, Color::Reset);
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().fg(Color::Rgb(0, 0, 255)));
dom.cascade_subtrees(&sheet, &[div]);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 0, 255));
}
#[test]
fn var_resolves_from_stylesheet() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("accent", "#ff0000")
.rule_unchecked("div", TuiStyle::new().fg(TuiColor::var("accent")));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn var_with_named_color_value() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("muted", "gray")
.rule_unchecked("div", TuiStyle::new().fg(TuiColor::var("muted")));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(128, 128, 128));
}
#[test]
fn var_with_rgb_function_value() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("accent", "rgb(10, 20, 30)")
.rule_unchecked("div", TuiStyle::new().fg(TuiColor::var("accent")));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(10, 20, 30));
}
#[test]
fn var_with_rgba_function_value_drops_alpha() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("translucent", "rgba(200, 100, 50, 0.5)")
.rule_unchecked("div", TuiStyle::new().fg(TuiColor::var("translucent")));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(200, 100, 50));
}
#[test]
fn fg_var_shorthand_setter() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("highlight", "blue")
.rule_unchecked("div", TuiStyle::new().fg_var("highlight"));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 0, 255));
}
#[test]
fn unresolved_var_falls_back_to_inherit() {
use crate::TuiColor;
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().fg(Color::Rgb(255, 0, 0)))
.rule_unchecked("span", TuiStyle::new().fg(TuiColor::var("missing")));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).fg, Color::Rgb(255, 0, 0));
}
#[test]
fn var_fallback_chain_at_cascade() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("b", "cyan")
.rule_unchecked(
"div",
TuiStyle::new().fg(TuiColor::var_with(
"a",
TuiColor::var_with("b", TuiColor::Literal(Color::Rgb(255, 0, 0))),
)),
);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).fg, Color::Rgb(0, 255, 255));
}
#[test]
fn bg_and_border_fg_also_support_var() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("panel", "#101020")
.define_var("edge", "#808080")
.rule_unchecked(
"div",
TuiStyle::new()
.bg(TuiColor::var("panel"))
.border_fg(TuiColor::var("edge")),
);
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert_eq!(c.bg, Color::Rgb(0x10, 0x10, 0x20));
assert_eq!(c.border_fg, Color::Rgb(0x80, 0x80, 0x80));
}
#[test]
fn var_works_in_pseudo_element() {
use crate::TuiColor;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.define_var("arrow-color", "#ff00ff")
.rule_unchecked(
"div::before",
TuiStyle::new()
.fg(TuiColor::var("arrow-color"))
.content(Content::Str("▾".into())),
);
dom.cascade(&sheet);
let before = dom
.node(div)
.ext()
.unwrap()
.computed_before
.as_ref()
.unwrap();
assert_eq!(before.fg, Color::Rgb(0xff, 0x00, 0xff));
}
#[test]
fn display_defaults_to_block() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
assert_eq!(computed_of(&dom, div).display, Display::Block);
}
#[test]
fn display_does_not_inherit() {
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().display(Display::Inline));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).display, Display::Inline);
assert_eq!(computed_of(&dom, child).display, Display::Block);
}
#[test]
fn display_inline_via_rule() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().display(Display::Inline));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).display, Display::Inline);
}
#[test]
fn white_space_defaults_to_normal() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
assert_eq!(computed_of(&dom, div).white_space, WhiteSpace::Normal);
}
#[test]
fn white_space_inherits() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("pre");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("pre", TuiStyle::new().white_space(WhiteSpace::Pre));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).white_space, WhiteSpace::Pre);
assert_eq!(computed_of(&dom, child).white_space, WhiteSpace::Pre);
}
#[test]
fn white_space_child_overrides_inherited() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("pre");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("pre", TuiStyle::new().white_space(WhiteSpace::Pre))
.rule_unchecked("span", TuiStyle::new().white_space(WhiteSpace::Normal));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).white_space, WhiteSpace::Normal);
}
#[test]
fn display_and_white_space_mark_layout_dirty() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
dom.node_mut(div).ext_mut().unwrap().layout_dirty = false;
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().display(Display::Inline));
dom.cascade(&sheet);
assert!(dom.node(div).ext().unwrap().layout_dirty);
dom.node_mut(div).ext_mut().unwrap().layout_dirty = false;
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.display(Display::Inline)
.white_space(WhiteSpace::Pre),
);
dom.cascade(&sheet);
assert!(dom.node(div).ext().unwrap().layout_dirty);
}
#[test]
fn display_important_cascades_through_ladder() {
let (mut dom, div) = dom_with_div();
dom.node_mut(div)
.set_inline_style(TuiStyle::new().display(Display::Inline));
let sheet =
Stylesheet::bare().rule_unchecked("div", TuiStyle::new().display_important(Display::Block));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).display, Display::Block);
}
#[test]
fn user_select_defaults_to_auto() {
use crate::layout::UserSelect;
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
assert_eq!(computed_of(&dom, div).user_select, UserSelect::Auto);
}
#[test]
fn user_select_inherits_to_child() {
use crate::layout::UserSelect;
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().user_select(UserSelect::None));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).user_select, UserSelect::None);
assert_eq!(
computed_of(&dom, child).user_select,
UserSelect::None,
"user-select inherits"
);
}
#[test]
fn user_select_child_rule_overrides_inherited() {
use crate::layout::UserSelect;
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().user_select(UserSelect::None))
.rule_unchecked("span", TuiStyle::new().user_select(UserSelect::Text));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).user_select, UserSelect::Text);
}
#[test]
fn user_select_important_beats_inline() {
use crate::layout::UserSelect;
let (mut dom, div) = dom_with_div();
dom.node_mut(div)
.set_inline_style(TuiStyle::new().user_select(UserSelect::Text));
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().user_select_important(UserSelect::None),
);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).user_select, UserSelect::None);
}
fn ua_computed_for(tag: &str) -> ComputedStyle {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let el = dom.create_element(tag);
dom.append_child(root, el).unwrap();
dom.cascade(&Stylesheet::new());
computed_of(&dom, el)
}
#[test]
fn ua_mark_has_yellow_bg_and_black_fg() {
let c = ua_computed_for("mark");
assert_eq!(c.bg, Color::Rgb(255, 255, 0));
assert_eq!(c.fg, Color::Rgb(0, 0, 0));
assert_eq!(c.display, Display::Inline);
}
#[test]
fn ua_kbd_is_bold_accent_inline() {
let c = ua_computed_for("kbd");
assert_eq!(c.fg, Color::Rgb(30, 144, 255));
assert!(c.modifiers.contains(Modifier::BOLD));
assert_eq!(c.display, Display::Inline);
}
#[test]
fn ua_del_is_inline_and_strikethrough() {
let c = ua_computed_for("del");
assert_eq!(c.display, Display::Inline);
assert!(c.modifiers.contains(Modifier::CROSSED_OUT));
}
#[test]
fn ua_ins_is_inline_and_underlined() {
let c = ua_computed_for("ins");
assert_eq!(c.display, Display::Inline);
assert!(c.modifiers.contains(Modifier::UNDERLINED));
}
#[test]
fn ua_h4_through_h6_are_block_and_bold() {
for tag in ["h4", "h5", "h6"] {
let c = ua_computed_for(tag);
assert_eq!(c.display, Display::Block, "<{tag}> should be Block");
assert!(
c.modifiers.contains(Modifier::BOLD),
"<{tag}> should be bold"
);
}
}
#[test]
fn ua_cite_dfn_var_are_italic_inline() {
for tag in ["cite", "dfn", "var"] {
let c = ua_computed_for(tag);
assert_eq!(c.display, Display::Inline, "<{tag}> should be Inline");
assert!(
c.modifiers.contains(Modifier::ITALIC),
"<{tag}> should be italic"
);
}
}
#[test]
fn ua_blockquote_has_left_rail_and_is_muted() {
let c = ua_computed_for("blockquote");
assert_eq!(c.display, Display::Block);
assert_eq!(
c.padding.left,
crate::layout::PaddingValue::Cells(1),
"blockquote needs 1 cell padding"
);
assert_eq!(c.border, Border::left(), "blockquote shows `│` rail");
assert_eq!(c.fg, Color::Rgb(127, 134, 139));
}
#[test]
fn ua_ul_ol_menu_have_left_padding() {
for tag in ["ul", "ol", "menu"] {
let c = ua_computed_for(tag);
assert_eq!(
c.padding.left,
crate::layout::PaddingValue::Cells(2),
"<{tag}> needs list-marker room"
);
}
}
#[test]
fn ua_figcaption_is_italic_muted_block() {
let c = ua_computed_for("figcaption");
assert_eq!(c.display, Display::Block);
assert!(c.modifiers.contains(Modifier::ITALIC));
assert_eq!(c.fg, Color::Rgb(127, 134, 139));
}
#[test]
fn ua_sectioning_tags_are_block() {
for tag in [
"article", "section", "aside", "header", "footer", "main", "nav", "search",
] {
let c = ua_computed_for(tag);
assert_eq!(c.display, Display::Block, "<{tag}> should be Block");
}
}
#[test]
fn ua_interactive_block_tags_are_block() {
for tag in [
"details", "summary", "form", "fieldset", "legend", "dl", "dt", "dd",
] {
let c = ua_computed_for(tag);
assert_eq!(c.display, Display::Block, "<{tag}> should be Block");
}
for tag in ["summary", "dt", "legend"] {
let c = ua_computed_for(tag);
assert!(
c.modifiers.contains(Modifier::BOLD),
"<{tag}> should be bold"
);
}
}
#[test]
fn ua_dialog_is_none_when_closed() {
let c_closed = ua_computed_for("dialog");
assert_eq!(
c_closed.display,
Display::None,
"<dialog> without [open] should be None"
);
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("dialog");
dom.set_attribute(d, "open", "").unwrap();
dom.append_child(root, d).unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(
computed_of(&dom, d).display,
Display::Block,
"<dialog open> should be Block"
);
}
#[test]
fn ua_aria_tree_selectors_match() {
let mut dom: TuiDom = 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 collapsed = dom.create_element("li");
dom.set_attribute(collapsed, "role", "treeitem").unwrap();
dom.set_attribute(collapsed, "aria-expanded", "false")
.unwrap();
dom.append_child(tree, collapsed).unwrap();
let collapsed_group = dom.create_element("ul");
dom.set_attribute(collapsed_group, "role", "group").unwrap();
dom.append_child(collapsed, collapsed_group).unwrap();
let open = dom.create_element("li");
dom.set_attribute(open, "role", "treeitem").unwrap();
dom.set_attribute(open, "aria-expanded", "true").unwrap();
dom.append_child(tree, open).unwrap();
let open_group = dom.create_element("ul");
dom.set_attribute(open_group, "role", "group").unwrap();
dom.append_child(open, open_group).unwrap();
let active = dom.create_element("li");
dom.set_attribute(active, "role", "treeitem").unwrap();
dom.set_attribute(active, "data-rdom-active", "").unwrap();
dom.append_child(open_group, active).unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(
computed_of(&dom, collapsed_group).display,
Display::None,
"collapsed branch's group must be hidden",
);
assert_eq!(
computed_of(&dom, open_group).display,
Display::Block,
"expanded branch's group must stay block",
);
assert_eq!(
computed_of(&dom, active).bg,
Color::Reset,
"no highlight while the tree is unfocused",
);
dom.set_focused(Some(tree));
dom.cascade(&Stylesheet::new());
assert_eq!(
computed_of(&dom, active).bg,
Color::Rgb(0x2d, 0x2f, 0x31),
"active row highlights when [role=tree]:focus",
);
}
#[test]
fn ua_colgroup_and_col_are_none() {
assert_eq!(ua_computed_for("colgroup").display, Display::None);
assert_eq!(ua_computed_for("col").display, Display::None);
}
#[test]
fn ua_hidden_attribute_makes_element_none() {
let (mut dom, div) = dom_with_div();
dom.set_attribute(div, "hidden", "").unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).display, Display::None);
}
#[test]
fn ua_hidden_attribute_value_does_not_matter() {
let (mut dom, div) = dom_with_div();
dom.set_attribute(div, "hidden", "false").unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).display, Display::None);
}
#[test]
fn ua_hidden_attribute_can_be_overridden_by_author_rule() {
let (mut dom, div) = dom_with_div();
dom.set_attribute(div, "hidden", "").unwrap();
let sheet =
Stylesheet::new().rule_unchecked("[hidden]", TuiStyle::new().display(Display::Block));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).display, Display::Block);
}
#[test]
fn ua_hidden_does_not_apply_to_nonhidden() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).display, Display::Block);
}
#[test]
fn ua_optgroup_before_resolves_label_attribute_via_attr_content() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let og = dom.create_element("optgroup");
dom.set_attribute(og, "label", "Fruits").unwrap();
dom.append_child(root, og).unwrap();
dom.cascade(&Stylesheet::new());
use crate::node::TuiNodeExt;
let before = dom.node(og).computed_before().cloned().unwrap();
assert_eq!(before.content.as_deref(), Some("Fruits"));
}
#[test]
fn attr_content_falls_back_to_empty_when_attribute_missing() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let og = dom.create_element("optgroup");
dom.append_child(root, og).unwrap();
dom.cascade(&Stylesheet::new());
use crate::node::TuiNodeExt;
let before = dom.node(og).computed_before().cloned().unwrap();
assert_eq!(before.content.as_deref(), Some(""));
}
#[test]
fn placeholder_shown_triggers_before_content_with_placeholder_text() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.set_attribute(inp, "placeholder", "Search").unwrap();
dom.append_child(root, inp).unwrap();
dom.cascade(&Stylesheet::new());
use crate::node::TuiNodeExt;
let before = dom.node(inp).computed_before().cloned().unwrap();
assert_eq!(before.content.as_deref(), Some("Search"));
assert_eq!(before.fg, Color::Rgb(127, 134, 139));
}
#[test]
fn placeholder_disappears_when_input_has_text_content() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let inp = dom.create_element("input");
dom.set_attribute(inp, "type", "text").unwrap();
dom.set_attribute(inp, "placeholder", "Search").unwrap();
let t = dom.create_text_node("typed");
dom.append_child(inp, t).unwrap();
dom.append_child(root, inp).unwrap();
dom.cascade(&Stylesheet::new());
use crate::node::TuiNodeExt;
let before = dom.node(inp).computed_before();
let has_content = before
.and_then(|c| c.content.as_deref())
.map(|s| !s.is_empty())
.unwrap_or(false);
assert!(
!has_content,
"placeholder should not render when input has text"
);
}
#[test]
fn textarea_placeholder_shown_also_renders() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let ta = dom.create_element("textarea");
dom.set_attribute(ta, "placeholder", "Write something…")
.unwrap();
dom.append_child(root, ta).unwrap();
dom.cascade(&Stylesheet::new());
use crate::node::TuiNodeExt;
let before = dom.node(ta).computed_before().cloned().unwrap();
assert_eq!(before.content.as_deref(), Some("Write something…"));
}
#[test]
fn attr_content_is_overridable_by_author_rule() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let el = dom.create_element("div");
dom.set_attribute(el, "data-status", "warning").unwrap();
dom.append_child(root, el).unwrap();
let sheet = Stylesheet::new().rule_unchecked(
"div::before",
TuiStyle::new().content(Content::Attr("data-status".into())),
);
dom.cascade(&sheet);
use crate::node::TuiNodeExt;
let before = dom.node(el).computed_before().cloned().unwrap();
assert_eq!(before.content.as_deref(), Some("warning"));
}
#[test]
fn ua_author_rule_overrides_tier_1_default() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let mark = dom.create_element("mark");
dom.append_child(root, mark).unwrap();
let sheet =
Stylesheet::new().rule_unchecked("mark", TuiStyle::new().bg(Color::Rgb(0, 255, 255)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, mark).bg, Color::Rgb(0, 255, 255));
}
#[test]
fn ua_bare_a_is_inline_with_no_link_styling() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
dom.append_child(root, a).unwrap();
dom.cascade(&Stylesheet::new());
let c = computed_of(&dom, a);
assert_eq!(c.display, Display::Inline);
assert_eq!(c.fg, Color::Reset, "bare <a> should inherit, not colorize");
assert!(!c.modifiers.contains(Modifier::UNDERLINED));
}
#[test]
fn ua_a_with_href_is_link_styled() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
dom.set_attribute(a, "href", "https://example.com").unwrap();
dom.append_child(root, a).unwrap();
dom.cascade(&Stylesheet::new());
let c = computed_of(&dom, a);
assert_eq!(c.display, Display::Inline);
assert_eq!(c.fg, Color::Rgb(30, 144, 255));
assert!(c.modifiers.contains(Modifier::UNDERLINED));
}
#[test]
fn ua_a_href_hover_bolds() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
dom.set_attribute(a, "href", "/internal").unwrap();
dom.append_child(root, a).unwrap();
dom.set_hovered(Some(a));
dom.cascade(&Stylesheet::new());
let c = computed_of(&dom, a);
assert!(
c.modifiers.contains(Modifier::BOLD),
"a[href]:hover should bold for clickable feedback"
);
assert_eq!(c.fg, Color::Rgb(30, 144, 255));
assert!(c.modifiers.contains(Modifier::UNDERLINED));
}
#[test]
fn ua_a_without_href_does_not_get_hover_bold() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
dom.append_child(root, a).unwrap();
dom.set_hovered(Some(a));
dom.cascade(&Stylesheet::new());
let c = computed_of(&dom, a);
assert!(!c.modifiers.contains(Modifier::BOLD));
}
#[test]
fn position_default_is_static() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::new());
assert_eq!(
computed_of(&dom, div).position,
crate::layout::Position::Static
);
}
#[test]
fn position_relative_cascades() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().position(crate::layout::Position::Relative),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, div).position,
crate::layout::Position::Relative
);
}
#[test]
fn position_does_not_inherit() {
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().position(crate::layout::Position::Absolute),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, parent).position,
crate::layout::Position::Absolute
);
assert_eq!(
computed_of(&dom, child).position,
crate::layout::Position::Static
);
}
#[test]
fn top_left_offsets_cascade() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.top(crate::layout::Length::Cells(3))
.left(crate::layout::Length::Cells(-2)),
);
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert_eq!(c.top, crate::layout::Length::Cells(3));
assert_eq!(c.left, crate::layout::Length::Cells(-2));
assert_eq!(c.right, crate::layout::Length::Auto);
assert_eq!(c.bottom, crate::layout::Length::Auto);
}
#[test]
fn z_index_cascades() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().z_index(crate::layout::ZIndex::Value(7)),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, div).z_index,
crate::layout::ZIndex::Value(7)
);
}
#[test]
fn z_index_default_is_auto() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, div).z_index, crate::layout::ZIndex::Auto);
}
#[test]
fn position_important_beats_non_important() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new().position_important(crate::layout::Position::Absolute),
)
.rule_unchecked(
"div",
TuiStyle::new().position(crate::layout::Position::Relative),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, div).position,
crate::layout::Position::Absolute
);
}
#[test]
fn tree_flag_false_without_positioned_pseudo() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
let span = dom.create_element("span");
dom.append_child(div, span).unwrap();
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div::before",
TuiStyle::new().content(Content::Str("[ ".into())),
);
dom.cascade(&sheet);
assert!(!dom.node(div).ext().unwrap().tree_has_positioned_pseudo);
assert!(!dom.node(span).ext().unwrap().tree_has_positioned_pseudo);
}
#[test]
fn tree_flag_bubbles_to_ancestors() {
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();
dom.node_mut(inner).set_id("target").unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"#target::before",
TuiStyle::new()
.content(Content::Str("•".into()))
.position(crate::layout::Position::Absolute),
);
dom.cascade(&sheet);
assert!(dom.node(inner).ext().unwrap().tree_has_positioned_pseudo);
assert!(
dom.node(outer).ext().unwrap().tree_has_positioned_pseudo,
"outer must inherit the aggregate so the early-exit check fires"
);
}
#[test]
fn tree_flag_relative_after_pseudo_counts() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div::after",
TuiStyle::new()
.content(Content::Str(" ▶".into()))
.position(crate::layout::Position::Relative),
);
dom.cascade(&sheet);
assert!(dom.node(div).ext().unwrap().tree_has_positioned_pseudo);
}
#[test]
fn tree_flag_clears_on_full_recascade() {
let (mut dom, div) = dom_with_div();
let pseudo_sheet = Stylesheet::bare().rule_unchecked(
"div::before",
TuiStyle::new()
.content(Content::Str("[".into()))
.position(crate::layout::Position::Absolute),
);
dom.cascade(&pseudo_sheet);
assert!(dom.node(div).ext().unwrap().tree_has_positioned_pseudo);
let bare = Stylesheet::bare();
dom.cascade(&bare);
assert!(!dom.node(div).ext().unwrap().tree_has_positioned_pseudo);
}
#[test]
fn text_decoration_underline_sets_underlined_bit() {
use crate::layout::TextDecoration;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().text_decoration(TextDecoration::Underline),
);
dom.cascade(&sheet);
assert!(
computed_of(&dom, div)
.modifiers
.contains(Modifier::UNDERLINED)
);
assert!(
!computed_of(&dom, div)
.modifiers
.contains(Modifier::CROSSED_OUT)
);
}
#[test]
fn text_decoration_line_through_sets_crossed_out_bit() {
use crate::layout::TextDecoration;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().text_decoration(TextDecoration::LineThrough),
);
dom.cascade(&sheet);
assert!(
computed_of(&dom, div)
.modifiers
.contains(Modifier::CROSSED_OUT)
);
assert!(
!computed_of(&dom, div)
.modifiers
.contains(Modifier::UNDERLINED)
);
}
#[test]
fn text_decoration_none_clears_decoration_bits() {
use crate::layout::TextDecoration;
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new().text_decoration(TextDecoration::Underline),
)
.rule_unchecked("div", TuiStyle::new().text_decoration(TextDecoration::None));
dom.cascade(&sheet);
let c = computed_of(&dom, div);
assert!(!c.modifiers.contains(Modifier::UNDERLINED));
assert!(!c.modifiers.contains(Modifier::CROSSED_OUT));
}
#[test]
fn text_decoration_does_not_inherit_to_children() {
use crate::layout::TextDecoration;
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().text_decoration(TextDecoration::Underline),
);
dom.cascade(&sheet);
assert!(
computed_of(&dom, parent)
.modifiers
.contains(Modifier::UNDERLINED)
);
assert!(
!computed_of(&dom, child)
.modifiers
.contains(Modifier::UNDERLINED)
);
}
#[test]
fn opacity_setter_clamps_to_unit_interval() {
let s_neg = TuiStyle::new().opacity(-0.5);
let s_high = TuiStyle::new().opacity(1.5);
let s_half = TuiStyle::new().opacity(0.5);
if let Some(crate::style::Value::Specified(v)) = s_neg.opacity {
assert_eq!(v, 0.0);
} else {
panic!("opacity not set");
}
if let Some(crate::style::Value::Specified(v)) = s_high.opacity {
assert_eq!(v, 1.0);
} else {
panic!("opacity not set");
}
if let Some(crate::style::Value::Specified(v)) = s_half.opacity {
assert_eq!(v, 0.5);
} else {
panic!("opacity not set");
}
}
#[test]
fn opacity_cascade_initial_is_one() {
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
assert_eq!(computed_of(&dom, div).opacity, 1.0);
}
#[test]
fn opacity_cascade_applies_specified_value() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new().opacity(0.5));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).opacity, 0.5);
}
#[test]
fn opacity_does_not_inherit_to_child() {
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().opacity(0.5));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).opacity, 0.5);
assert_eq!(computed_of(&dom, child).opacity, 1.0);
}
#[test]
fn where_pseudo_applies_when_unopposed() {
let (mut dom, div) = dom_with_div();
let sheet =
Stylesheet::bare().rule_unchecked(":where(div)", TuiStyle::new().bg(Color::Rgb(9, 9, 9)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).bg, Color::Rgb(9, 9, 9));
}
#[test]
fn author_rule_beats_where_pseudo_regardless_of_source_order() {
let (mut dom, div) = dom_with_div();
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().bg(Color::Rgb(1, 2, 3)))
.rule_unchecked(":where(div)", TuiStyle::new().bg(Color::Rgb(9, 9, 9)));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, div).bg, Color::Rgb(1, 2, 3));
}
#[test]
fn focus_tint_applies_to_controls() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let button = dom.create_element("button");
dom.append_child(root, button).unwrap();
dom.set_focused(Some(button));
dom.cascade(&Stylesheet::new());
assert_eq!(
computed_of(&dom, button).bg,
Color::Rgb(0x2d, 0x2f, 0x31),
"a focused control gets the focus tint"
);
}
#[test]
fn focus_tint_does_not_fill_containers() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
dom.set_focused(Some(div));
dom.cascade(&Stylesheet::new());
assert_eq!(
computed_of(&dom, div).bg,
Color::Reset,
"a focused container must not be flooded with the focus tint"
);
}
#[test]
fn focused_scroll_container_thumb_is_accent() {
use crate::node::TuiNodeExt;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let d = dom.create_element("div");
dom.node_mut(d).set_inline_style(
TuiStyle::new()
.height(Size::Fixed(4))
.overflow_y(Overflow::Scroll),
);
dom.append_child(root, d).unwrap();
dom.set_focused(Some(d));
dom.cascade(&Stylesheet::new());
let thumb_fg = dom
.node(d)
.tui_ext()
.and_then(|e| e.computed_scrollbar_thumb.as_ref())
.map(|c| c.fg);
assert_eq!(
thumb_fg,
Some(Color::Rgb(30, 144, 255)), "a focused scroll container's thumb glyph turns accent (foreground)"
);
}