use super::*;
use crate::layout::{Border, Direction, Display, Flow, Overflow, Padding, Size, WhiteSpace};
use crate::style::{Color, Content, Modifier, Stylesheet, TuiStyle, Value};
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: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("input");
dom.set_attribute(div, "disabled", "").unwrap();
dom.append_child(root, div).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: TuiDom = TuiDom::new();
let root = dom.root();
let input = dom.create_element("input");
dom.set_attribute(input, "disabled", "").unwrap();
dom.append_child(root, input).unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(computed_of(&dom, input).fg, Color::Rgb(127, 134, 139));
}
#[test]
fn ua_disabled_rule_ignores_a_disabled_attribute_on_a_non_control() {
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::Reset);
}
#[test]
fn ua_disabled_rule_covers_controls_in_a_disabled_fieldset() {
use crate::layout::UserSelect;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let fs = dom.create_element("fieldset");
dom.set_attribute(fs, "disabled", "").unwrap();
dom.append_child(root, fs).unwrap();
let input = dom.create_element("input");
dom.append_child(fs, input).unwrap();
let button = dom.create_element("button");
dom.append_child(fs, button).unwrap();
dom.cascade(&Stylesheet::new());
for c in [fs, input, button] {
assert_eq!(computed_of(&dom, c).fg, Color::Rgb(127, 134, 139));
assert_eq!(computed_of(&dom, c).user_select, UserSelect::None);
}
}
#[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, rdom_style::layout::GapValue::Cells(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_is_not_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));
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).user_select, UserSelect::None);
assert_eq!(
computed_of(&dom, child).user_select,
UserSelect::Auto,
"user-select does not inherit"
);
assert_eq!(
crate::style::user_select::used_value(&dom, child),
UserSelect::None,
"the used value of `auto` under a `none` parent is `none`"
);
}
#[test]
fn user_select_child_rule_overrides_parent() {
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"
);
}
fn placeholder_box(sheet: &Stylesheet, text: Option<&str>) -> Option<crate::style::ComputedStyle> {
use crate::node::TuiNodeExt;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let inp = dom.create_element("input");
dom.set_attribute(inp, "placeholder", "Search").unwrap();
if let Some(t) = text {
let t = dom.create_text_node(t);
dom.append_child(inp, t).unwrap();
}
dom.append_child(root, inp).unwrap();
dom.cascade(sheet);
dom.node(inp).computed_before().cloned()
}
#[test]
fn author_placeholder_rule_styles_the_placeholder_text() {
let ua = placeholder_box(&Stylesheet::new(), None).unwrap();
assert_eq!(ua.fg, Color::Rgb(127, 134, 139), "UA default");
let red = Color::Rgb(255, 0, 0);
let sheet =
Stylesheet::new().rule_unchecked("input::placeholder", TuiStyle::new().fg(red).bold(true));
let styled = placeholder_box(&sheet, None).unwrap();
assert_eq!(styled.content.as_deref(), Some("Search"));
assert_eq!(styled.fg, red);
assert!(styled.modifiers.contains(Modifier::BOLD));
}
#[test]
fn placeholder_style_disappears_once_the_field_has_a_value() {
let red = Color::Rgb(255, 0, 0);
let sheet = Stylesheet::new().rule_unchecked("input::placeholder", TuiStyle::new().fg(red));
let filled = placeholder_box(&sheet, Some("typed"));
assert!(
filled.is_none_or(|b| b.fg != red && b.content.as_deref().is_none_or(str::is_empty)),
"no placeholder box once the field has text"
);
}
#[test]
fn placeholder_rules_cascade_by_specificity_then_order() {
let red = Color::Rgb(255, 0, 0);
let blue = Color::Rgb(0, 0, 255);
let sheet = Stylesheet::new()
.rule_unchecked(
"input:placeholder-shown::placeholder",
TuiStyle::new().fg(blue),
)
.rule_unchecked("input::placeholder", TuiStyle::new().fg(red));
assert_eq!(placeholder_box(&sheet, None).unwrap().fg, blue);
let sheet = Stylesheet::new()
.rule_unchecked("input::placeholder", TuiStyle::new().fg(red))
.rule_unchecked("input::placeholder", TuiStyle::new().fg(blue));
assert_eq!(placeholder_box(&sheet, None).unwrap().fg, blue);
}
#[test]
fn placeholder_rule_cannot_set_content() {
let sheet = Stylesheet::new().rule_unchecked(
"input::placeholder",
TuiStyle::new().content(Content::Str("other".into())),
);
let b = placeholder_box(&sheet, None).unwrap();
assert_eq!(b.content.as_deref(), Some("Search"));
}
#[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 marked_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();
let thumb_fg = |dom: &TuiDom| {
dom.node(d)
.tui_ext()
.and_then(|e| e.computed_scrollbar_thumb_vertical.as_ref())
.map(|c| c.fg)
};
dom.set_focused(Some(d));
dom.cascade(&Stylesheet::new());
assert_ne!(
thumb_fg(&dom),
Some(Color::Rgb(30, 144, 255)),
"focus alone does not color the thumb; the runtime's marker does"
);
dom.set_attribute(d, crate::runtime::scrollbar::SCROLL_FOCUS_ATTR, "")
.unwrap();
dom.cascade(&Stylesheet::new());
assert_eq!(
thumb_fg(&dom),
Some(Color::Rgb(30, 144, 255)), "the marked scroll container's thumb glyph turns accent (foreground)"
);
}
fn parent_child() -> (TuiDom, NodeId, NodeId) {
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();
(dom, parent, child)
}
#[test]
fn opacity_inherit_takes_parent_value_and_initial_resets() {
let (mut dom, parent, child) = parent_child();
let mut inherit = TuiStyle::new();
inherit.opacity = Some(Value::Inherit);
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().opacity(0.5))
.rule_unchecked("span", inherit);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, parent).opacity, 0.5);
assert_eq!(
computed_of(&dom, child).opacity,
0.5,
"inherit → parent's 0.5"
);
let mut initial = TuiStyle::new();
initial.opacity = Some(Value::Initial);
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().opacity(0.5))
.rule_unchecked("span", initial);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).opacity, 1.0, "initial → 1");
}
#[test]
fn text_decoration_inherit_copies_parents_bits() {
use crate::layout::TextDecoration;
let (mut dom, parent, child) = parent_child();
let mut inherit = TuiStyle::new();
inherit.text_decoration = Some(Value::Inherit);
let mut underline = TuiStyle::new();
underline.text_decoration = Some(Value::Specified(TextDecoration::Underline));
let sheet = Stylesheet::bare()
.rule_unchecked("div", underline)
.rule_unchecked("span", inherit);
dom.cascade(&sheet);
assert!(
computed_of(&dom, parent)
.modifiers
.contains(Modifier::UNDERLINED)
);
assert!(
computed_of(&dom, child)
.modifiers
.contains(Modifier::UNDERLINED),
"explicit inherit copies the parent's underline"
);
}
#[test]
fn scrollbar_gutter_keywords_resolve() {
use crate::layout::ScrollbarGutter;
let (mut dom, parent, child) = parent_child();
let mut stable = TuiStyle::new();
stable.scrollbar_gutter = Some(Value::Specified(ScrollbarGutter::Stable));
let mut inherit = TuiStyle::new();
inherit.scrollbar_gutter = Some(Value::Inherit);
let sheet = Stylesheet::bare()
.rule_unchecked("div", stable.clone())
.rule_unchecked("span", inherit);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, parent).scrollbar_gutter,
ScrollbarGutter::Stable
);
assert_eq!(
computed_of(&dom, child).scrollbar_gutter,
ScrollbarGutter::Stable
);
let mut initial = TuiStyle::new();
initial.scrollbar_gutter = Some(Value::Initial);
let sheet = Stylesheet::bare()
.rule_unchecked("div", stable)
.rule_unchecked("span", initial);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, child).scrollbar_gutter,
ScrollbarGutter::Auto
);
}
#[test]
fn scroll_behavior_cascades_and_resolves_css_wide_keywords() {
use crate::layout::ScrollBehavior;
let (mut dom, parent, child) = parent_child();
let smooth = || TuiStyle::new().scroll_behavior(ScrollBehavior::Smooth);
let sheet = Stylesheet::bare().rule_unchecked("div", smooth());
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, parent).scroll_behavior,
ScrollBehavior::Smooth
);
assert_eq!(
computed_of(&dom, child).scroll_behavior,
ScrollBehavior::Auto,
"not inherited"
);
for (kw, want) in [
("inherit", ScrollBehavior::Smooth),
("initial", ScrollBehavior::Auto),
("unset", ScrollBehavior::Auto),
] {
let mut span = smooth();
rdom_style::property_dispatch::set("scroll-behavior", kw, &mut span).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("div", smooth())
.rule_unchecked("span", span);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, child).scroll_behavior, want, "{kw:?}");
}
}
#[test]
fn cascade_inherits_exactly_the_style_crates_inherited_set() {
use super::inherit::inherit_inheritable_from;
use rdom_style::layout::{
AspectRatio, Border, BorderCollapse, BorderStyle, CaretColor, CaretTextColor, Direction,
Display, Length, Margin, MinSize, Overflow, Padding, PointerEvents, Position,
ScrollBehavior, ScrollbarGutter, Size, UserSelect, WhiteSpace, ZIndex,
};
use rdom_style::property_dispatch::{inherits, property_names};
use rdom_style::transition::{TimingFunction, TransitionProperty};
use std::collections::HashMap;
use std::rc::Rc;
let mut parent = ComputedStyle::initial();
parent.fg = Color::Rgb(1, 2, 3);
parent.bg = Color::Rgb(4, 5, 6);
parent.border_fg = Color::Rgb(7, 8, 9);
parent.modifiers = Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED;
parent.opacity = 0.5;
parent.width = Size::Fixed(7);
parent.height = Size::Fixed(8);
parent.min_width = Some(MinSize::Cells(1));
parent.max_width = Some(9);
parent.min_height = Some(MinSize::Cells(1));
parent.max_height = Some(9);
parent.aspect_ratio = AspectRatio::new(4, 3);
parent.padding = Padding::all(1);
parent.margin = Margin::all_cells(1);
parent.gap = rdom_style::layout::GapValue::Cells(2);
parent.flex_shrink = 3;
parent.border = Border {
top: BorderStyle::Solid,
..Border::default()
};
parent.border_collapse = BorderCollapse::Collapse;
parent.direction = Direction::Row;
parent.overflow_x = Overflow::Hidden;
parent.overflow_y = Overflow::Scroll;
parent.scrollbar_gutter = ScrollbarGutter::Stable;
parent.scroll_behavior = ScrollBehavior::Smooth;
parent.display = Display::Inline;
parent.white_space = WhiteSpace::Pre;
parent.user_select = UserSelect::None;
parent.pointer_events = PointerEvents::None;
parent.caret_color = CaretColor::Transparent;
parent.caret_text_color = CaretTextColor::Color(Color::Rgb(1, 1, 1).into());
parent.content = Some("x".into());
parent.position = Position::Relative;
parent.top = Length::Cells(1);
parent.right = Length::Cells(1);
parent.bottom = Length::Cells(1);
parent.left = Length::Cells(1);
parent.z_index = ZIndex::Value(1);
parent.transition_property = vec![TransitionProperty::All];
parent.transition_duration = vec![100];
parent.transition_timing_function = vec![TimingFunction::Linear];
parent.transition_delay = vec![5];
parent.vars = Rc::new(HashMap::from([("a".to_string(), "b".to_string())]));
let mut child = ComputedStyle::initial();
inherit_inheritable_from(&mut child, &parent);
let probes: &[(&str, bool)] = &[
("color", child.fg == parent.fg),
("background-color", child.bg == parent.bg),
("border-color", child.border_fg == parent.border_fg),
("font-weight", child.modifiers.contains(Modifier::BOLD)),
("font-style", child.modifiers.contains(Modifier::ITALIC)),
(
"text-decoration",
child.modifiers.contains(Modifier::UNDERLINED),
),
("opacity", child.opacity == parent.opacity),
("width", child.width == parent.width),
("height", child.height == parent.height),
("min-width", child.min_width == parent.min_width),
("max-width", child.max_width == parent.max_width),
("min-height", child.min_height == parent.min_height),
("max-height", child.max_height == parent.max_height),
("aspect-ratio", child.aspect_ratio == parent.aspect_ratio),
("padding", child.padding == parent.padding),
("margin", child.margin == parent.margin),
("gap", child.gap == parent.gap),
("flex-shrink", child.flex_shrink == parent.flex_shrink),
("border", child.border == parent.border),
(
"border-collapse",
child.border_collapse == parent.border_collapse,
),
("flex-direction", child.direction == parent.direction),
("overflow-x", child.overflow_x == parent.overflow_x),
("overflow-y", child.overflow_y == parent.overflow_y),
(
"scrollbar-gutter",
child.scrollbar_gutter == parent.scrollbar_gutter,
),
(
"scroll-behavior",
child.scroll_behavior == parent.scroll_behavior,
),
("display", child.display == parent.display),
("white-space", child.white_space == parent.white_space),
("user-select", child.user_select == parent.user_select),
(
"pointer-events",
child.pointer_events == parent.pointer_events,
),
("caret-color", child.caret_color == parent.caret_color),
(
"caret-text-color",
child.caret_text_color == parent.caret_text_color,
),
("content", child.content == parent.content),
("position", child.position == parent.position),
("top", child.top == parent.top),
("right", child.right == parent.right),
("bottom", child.bottom == parent.bottom),
("left", child.left == parent.left),
("z-index", child.z_index == parent.z_index),
(
"transition-property",
child.transition_property == parent.transition_property,
),
(
"transition-duration",
child.transition_duration == parent.transition_duration,
),
(
"transition-timing-function",
child.transition_timing_function == parent.transition_timing_function,
),
(
"transition-delay",
child.transition_delay == parent.transition_delay,
),
];
for (name, took) in probes {
assert_eq!(
*took,
inherits(name),
"{name}: the cascade {} it but the style table says {}",
if *took {
"inherited"
} else {
"did not inherit"
},
inherits(name)
);
}
for name in property_names().iter().filter(|n| inherits(n)) {
assert!(
probes.iter().any(|(p, _)| p == name),
"{name} inherits but is not probed"
);
}
assert!(Rc::ptr_eq(&child.vars, &parent.vars));
}
#[test]
fn transition_inherit_and_initial_resolve_against_the_parent() {
use rdom_style::Value;
use rdom_style::transition::{TimingFunction, TransitionProperty};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let inheriting = dom.create_element("i");
let resetting = dom.create_element("b");
let plain = dom.create_element("u");
dom.append_child(root, parent).unwrap();
for c in [inheriting, resetting, plain] {
dom.append_child(parent, c).unwrap();
}
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.transition_property(vec![TransitionProperty::All])
.transition_duration(vec![250])
.transition_timing_function(vec![TimingFunction::Linear])
.transition_delay(vec![10]),
)
.rule_unchecked(
"i",
TuiStyle {
transition_property: Some(Value::Inherit),
transition_duration: Some(Value::Inherit),
transition_timing_function: Some(Value::Inherit),
transition_delay: Some(Value::Inherit),
..TuiStyle::new()
},
)
.rule_unchecked(
"b",
TuiStyle::new()
.transition_duration(vec![5])
.transition_property(vec![TransitionProperty::All]),
)
.rule_unchecked(
"b",
TuiStyle {
transition_duration: Some(Value::Initial),
..TuiStyle::new()
},
);
dom.cascade(&sheet);
let p = computed_of(&dom, parent);
let i = computed_of(&dom, inheriting);
assert_eq!(i.transition_property, p.transition_property);
assert_eq!(i.transition_duration, vec![250]);
assert_eq!(i.transition_timing_function, vec![TimingFunction::Linear]);
assert_eq!(i.transition_delay, vec![10]);
let b = computed_of(&dom, resetting);
assert_eq!(b.transition_property, vec![TransitionProperty::All]);
assert!(
b.transition_duration.is_empty(),
"initial is the empty list"
);
let u = computed_of(&dom, plain);
assert!(
u.transition_property.is_empty(),
"transitions do not inherit by default"
);
}
#[test]
fn custom_properties_scope_per_element_and_inherit() {
use rdom_style::TuiColor;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let card = dom.create_element("div");
let inner = dom.create_element("p");
let nested = dom.create_element("section");
let nested_p = dom.create_element("p");
let inline_p = dom.create_element("p");
let outside = dom.create_element("p");
dom.set_attribute(card, "class", "card").unwrap();
dom.set_attribute(nested, "class", "nested").unwrap();
dom.set_attribute(inline_p, "class", "inline").unwrap();
dom.append_child(root, card).unwrap();
dom.append_child(card, inner).unwrap();
dom.append_child(card, nested).unwrap();
dom.append_child(nested, nested_p).unwrap();
dom.append_child(card, inline_p).unwrap();
dom.append_child(root, outside).unwrap();
dom.node_mut(inline_p)
.set_inline_style(TuiStyle::new().custom_property("accent", "rgb(0, 0, 255)"));
let sheet = Stylesheet::bare()
.rule_unchecked(
".card",
TuiStyle::new().custom_property("accent", "rgb(255, 0, 0)"),
)
.rule_unchecked(
".nested",
TuiStyle::new().custom_property("accent", "rgb(0, 255, 0)"),
)
.rule_unchecked(
"p",
TuiStyle::new().fg(TuiColor::var_with("accent", Color::Rgb(9, 9, 9).into())),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, inner).fg,
Color::Rgb(255, 0, 0),
"inherited from .card"
);
assert_eq!(
computed_of(&dom, nested_p).fg,
Color::Rgb(0, 255, 0),
"nested override"
);
assert_eq!(
computed_of(&dom, inline_p).fg,
Color::Rgb(0, 0, 255),
"inline declaration"
);
assert_eq!(
computed_of(&dom, outside).fg,
Color::Rgb(9, 9, 9),
"not in scope → fallback"
);
let mut important = TuiStyle::new();
important.set_custom_property("accent", "rgb(1, 2, 3)", true);
let sheet = Stylesheet::bare()
.rule_unchecked(".card", important)
.rule_unchecked(
".card",
TuiStyle::new().custom_property("accent", "rgb(7, 7, 7)"),
)
.rule_unchecked(
"p",
TuiStyle::new().fg(TuiColor::var_with("accent", Color::Rgb(9, 9, 9).into())),
);
dom.cascade(&sheet);
assert_eq!(computed_of(&dom, inner).fg, Color::Rgb(1, 2, 3));
}
#[test]
fn counters_reset_increment_and_read_in_tree_order() {
use rdom_style::{CounterOp, CounterStyle};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let doc = dom.create_element("doc");
let a = dom.create_element("s");
let b = dom.create_element("s");
let c = dom.create_element("s");
dom.append_child(root, doc).unwrap();
for s in [a, b, c] {
dom.append_child(doc, s).unwrap();
}
let op = |name: &str, value: i32| CounterOp {
name: name.into(),
value,
};
let sheet = Stylesheet::bare()
.rule_unchecked("doc", TuiStyle::new().counter_reset(vec![op("sec", 0)]))
.rule_unchecked("s", TuiStyle::new().counter_increment(vec![op("sec", 1)]))
.rule_unchecked(
"s::before",
TuiStyle::new().content(Content::Concat(vec![
Content::Counter {
name: "sec".into(),
style: CounterStyle::UpperRoman,
},
Content::Str(". ".into()),
])),
)
.rule_unchecked(
"doc::after",
TuiStyle::new().content(Content::Counter {
name: "sec".into(),
style: CounterStyle::Decimal,
}),
);
dom.cascade(&sheet);
fn before(dom: &TuiDom, id: NodeId) -> Option<String> {
dom.node(id)
.ext()
.and_then(|e| e.computed_before.as_ref())
.and_then(|p| p.content.clone())
}
assert_eq!(before(&dom, a).as_deref(), Some("I. "));
assert_eq!(before(&dom, b).as_deref(), Some("II. "));
assert_eq!(before(&dom, c).as_deref(), Some("III. "));
let after_doc = dom
.node(doc)
.ext()
.and_then(|e| e.computed_after.as_ref())
.and_then(|p| p.content.clone());
assert_eq!(
after_doc.as_deref(),
Some("3"),
"::after sees the children's increments"
);
dom.cascade_subtrees(&sheet, &[c]);
assert_eq!(before(&dom, c).as_deref(), Some("III. "));
}
#[test]
fn nested_ul_does_not_advance_the_enclosing_ol_numbering() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let ol = dom.create_element("ol");
let a = dom.create_element("li");
let ul = dom.create_element("ul");
let x = dom.create_element("li");
let y = dom.create_element("li");
let b = dom.create_element("li");
dom.append_child(root, ol).unwrap();
dom.append_child(ol, a).unwrap();
dom.append_child(a, ul).unwrap();
dom.append_child(ul, x).unwrap();
dom.append_child(ul, y).unwrap();
dom.append_child(ol, b).unwrap();
dom.cascade(&Stylesheet::new());
let marker = |id: NodeId| {
dom.node(id)
.ext()
.and_then(|e| e.computed_before.as_ref())
.and_then(|p| p.content.clone())
};
assert_eq!(marker(a).as_deref(), Some("1. "));
assert_eq!(marker(b).as_deref(), Some("2. "));
assert_eq!(marker(x).as_deref(), Some("• "));
}
#[test]
fn subtree_cascade_renumbers_after_insertion_regardless_of_root_order() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let ol = dom.create_element("ol");
dom.append_child(root, ol).unwrap();
let mut items = Vec::new();
for text in ["a", "b", "c"] {
let li = dom.create_element("li");
let t = dom.create_text_node(text);
dom.append_child(li, t).unwrap();
dom.append_child(ol, li).unwrap();
items.push(li);
}
let sheet = Stylesheet::new();
dom.cascade(&sheet);
let marker = |dom: &TuiDom, id: NodeId| {
dom.node(id)
.ext()
.and_then(|e| e.computed_before.as_ref())
.and_then(|p| p.content.clone())
};
assert_eq!(marker(&dom, items[2]).as_deref(), Some("3. "));
let inserted = dom.create_element("li");
let t = dom.create_text_node("new");
dom.append_child(inserted, t).unwrap();
dom.insert_before(ol, inserted, Some(items[1])).unwrap();
for order in [
vec![items[0], items[1], items[2], inserted],
vec![inserted, items[2], items[1], items[0]],
] {
dom.cascade_subtrees(&sheet, &order);
assert_eq!(marker(&dom, items[0]).as_deref(), Some("1. "));
assert_eq!(marker(&dom, inserted).as_deref(), Some("2. "));
assert_eq!(marker(&dom, items[1]).as_deref(), Some("3. "));
assert_eq!(marker(&dom, items[2]).as_deref(), Some("4. "));
}
}
#[test]
fn pseudo_element_counter_ops_apply() {
use rdom_style::{CounterOp, CounterStyle};
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let doc = dom.create_element("doc");
let h1 = dom.create_element("h2");
let h2 = dom.create_element("h2");
dom.append_child(root, doc).unwrap();
dom.append_child(doc, h1).unwrap();
dom.append_child(doc, h2).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"doc",
TuiStyle::new().counter_reset(vec![CounterOp {
name: "sec".into(),
value: 0,
}]),
)
.rule_unchecked(
"h2::before",
TuiStyle::new()
.counter_increment(vec![CounterOp {
name: "sec".into(),
value: 1,
}])
.content(Content::Counter {
name: "sec".into(),
style: CounterStyle::Decimal,
}),
);
dom.cascade(&sheet);
let marker = |id: NodeId| {
dom.node(id)
.ext()
.and_then(|e| e.computed_before.as_ref())
.and_then(|p| p.content.clone())
};
assert_eq!(marker(h1).as_deref(), Some("1"));
assert_eq!(marker(h2).as_deref(), Some("2"));
}
#[test]
fn custom_property_css_wide_keywords() {
use rdom_style::TuiColor;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("div");
let reset = dom.create_element("p");
let inh = dom.create_element("b");
dom.append_child(root, outer).unwrap();
dom.append_child(outer, reset).unwrap();
dom.append_child(outer, inh).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().custom_property("c", "rgb(1, 1, 1)"))
.rule_unchecked("p", TuiStyle::new().custom_property("c", "initial"))
.rule_unchecked("b", TuiStyle::new().custom_property("c", "rgb(2, 2, 2)"))
.rule_unchecked("b", TuiStyle::new().custom_property("c", "inherit"))
.rule_unchecked(
"p, b",
TuiStyle::new().fg(TuiColor::var_with("c", Color::Rgb(9, 9, 9).into())),
);
dom.cascade(&sheet);
assert_eq!(
computed_of(&dom, reset).fg,
Color::Rgb(9, 9, 9),
"initial → undefined → fallback"
);
assert_eq!(
computed_of(&dom, inh).fg,
Color::Rgb(1, 1, 1),
"inherit → parent's value"
);
}
#[test]
fn detached_dirty_root_does_not_starve_connected_roots() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let ol = dom.create_element("ol");
dom.append_child(root, ol).unwrap();
let li = dom.create_element("li");
dom.append_child(ol, li).unwrap();
let old_demo = dom.create_element("section");
let old_child = dom.create_element("p");
dom.append_child(old_demo, old_child).unwrap();
dom.append_child(root, old_demo).unwrap();
let sheet = Stylesheet::new();
dom.cascade(&sheet);
dom.remove_child(root, old_demo).unwrap();
let sheet2 = Stylesheet::new().rule_unchecked("li", TuiStyle::new().fg(Color::Rgb(7, 7, 7)));
dom.cascade_subtrees(&sheet2, &[old_demo, old_child, ol, li]);
assert_eq!(
computed_of(&dom, li).fg,
Color::Rgb(7, 7, 7),
"connected root was cascaded"
);
let marker = dom
.node(li)
.ext()
.and_then(|e| e.computed_before.as_ref())
.and_then(|p| p.content.clone());
assert_eq!(marker.as_deref(), Some("1. "));
}
#[test]
fn layout_dirty_flag_reacts_to_positioning_changes() {
use crate::layout::{Length, Position};
use crate::node::TuiNodeExt;
use crate::render::LayoutExt;
let (mut dom, div) = dom_with_div();
dom.cascade(&Stylesheet::bare());
dom.layout_dom(crate::render::Rect::new(0, 0, 20, 5));
dom.cascade(&Stylesheet::bare());
assert!(
!dom.node(div).is_layout_dirty(),
"unchanged cascade is clean"
);
let positioned = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.position(Position::Absolute)
.top(Length::Cells(2)),
);
dom.cascade(&positioned);
assert!(
dom.node(div).is_layout_dirty(),
"position/top change dirties layout"
);
dom.cascade(&positioned);
let moved = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.position(Position::Absolute)
.top(Length::Cells(3)),
);
dom.cascade(&moved);
assert!(
dom.node(div).is_layout_dirty(),
"inset change dirties layout"
);
}
#[test]
fn ua_select_size_one_renders_as_a_dropdown() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let plain = dom.create_element("select");
let one = dom.create_element("select");
let list = dom.create_element("select");
dom.set_attribute(one, "size", "1").unwrap();
dom.set_attribute(list, "size", "3").unwrap();
for s in [plain, one, list] {
dom.append_child(root, s).unwrap();
}
dom.cascade(&Stylesheet::new());
let (p, o, l) = (
computed_of(&dom, plain),
computed_of(&dom, one),
computed_of(&dom, list),
);
assert_eq!(
(o.height.clone(), o.overflow_y),
(p.height.clone(), p.overflow_y),
"size=1 == dropdown chrome"
);
assert_ne!(
(l.height.clone(), l.overflow_y),
(p.height.clone(), p.overflow_y),
"size=3 is the list box"
);
}