use std::{collections::HashMap, rc::Rc, str::FromStr};
use cssparser::{Parser, ParserInput};
use serde_json::{from_value, json};
use taffy::Size;
use super::stylesheets_vars::resolve_var_references;
use super::{
CssWideKeyword, LonghandId, PropertyId, PropertyMask, ShorthandId, StyleDeclarationBlock,
};
use crate::{
Viewport,
style::{CalcArena, ComputedStyle, SizingContext, Style, StyleDeclaration, properties::*},
};
fn style_with(declarations: impl IntoIterator<Item = StyleDeclaration>) -> Style {
let mut style = Style::default();
for declaration in declarations {
style.push(declaration, false);
}
style
}
fn parse_declarations(name: &str, css: &str) -> StyleDeclarationBlock {
let mut input = ParserInput::new(css);
let mut parser = Parser::new(&mut input);
let declarations_result = StyleDeclarationBlock::parse(name, &mut parser);
assert!(declarations_result.is_ok());
declarations_result.unwrap_or_default()
}
fn parse_declarations_is_err(name: &str, css: &str) -> bool {
let mut input = ParserInput::new(css);
let mut parser = Parser::new(&mut input);
StyleDeclarationBlock::parse(name, &mut parser).is_err()
}
fn inherited_style_from_pairs(
declarations: impl IntoIterator<Item = (&'static str, &'static str)>,
parent: &ComputedStyle,
) -> ComputedStyle {
let mut style = Style::default();
for (name, value) in declarations {
style.append_block(parse_declarations(name, value));
}
style.inherit(parent)
}
fn resolve_var(
specified_value: &str,
custom_properties: impl IntoIterator<Item = (&'static str, &'static str)>,
) -> Option<String> {
let custom_properties = custom_properties
.into_iter()
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.collect::<HashMap<_, _>>();
resolve_var_references(specified_value, &custom_properties, &mut Vec::new())
}
#[test]
fn test_merge_from_inline_over_tailwind() {
let mut tw_style = style_with([
StyleDeclaration::width(Length::Rem(10.0)),
StyleDeclaration::height(Length::Rem(20.0)),
StyleDeclaration::color(ColorInput::Value(Color([255, 0, 0, 255]))),
]);
let inline_style = style_with([StyleDeclaration::width(Length::Px(100.0))]);
tw_style.merge_from(inline_style);
let resolved = tw_style.inherit(&ComputedStyle::default());
assert_eq!(resolved.width, Length::Px(100.0));
assert_eq!(resolved.height, Length::Rem(20.0));
assert_eq!(resolved.color, ColorInput::Value(Color([255, 0, 0, 255])));
}
#[test]
fn test_deserialize_numeric_opacity_preserves_fraction() -> Result<(), serde_json::Error> {
let style = from_value::<Style>(json!({ "opacity": 0.3 }))?;
let computed = style.inherit(&ComputedStyle::default());
assert_eq!(computed.opacity, PercentageNumber(0.3));
Ok(())
}
#[test]
fn property_id_accepts_kebab_and_camel_case() {
let padding_left_kebab = PropertyId::from_kebab_case("padding-left");
let padding_left_camel = PropertyId::from_camel_case("paddingLeft");
assert_ne!(padding_left_kebab, PropertyId::Ignored);
assert_ne!(padding_left_camel, PropertyId::Ignored);
assert_eq!(padding_left_kebab, padding_left_camel);
let webkit_text_fill_color_kebab = PropertyId::from_kebab_case("-webkit-text-fill-color");
let webkit_text_fill_color_camel = PropertyId::from_camel_case("WebkitTextFillColor");
assert_ne!(webkit_text_fill_color_kebab, PropertyId::Ignored);
assert_ne!(webkit_text_fill_color_camel, PropertyId::Ignored);
assert_eq!(
webkit_text_fill_color_kebab,
PropertyId::Longhand(LonghandId::WebkitTextFillColor)
);
assert_eq!(
webkit_text_fill_color_camel,
PropertyId::Longhand(LonghandId::WebkitTextFillColor)
);
}
#[test]
fn custom_properties_map_to_custom_property_id() {
assert_eq!(
PropertyId::from_kebab_case("--padding-left"),
PropertyId::Custom
);
assert_eq!(
PropertyId::from_kebab_case("--webkit-mask-image"),
PropertyId::Custom
);
}
#[test]
fn property_id_accepts_webkit_aliases() {
let line_clamp = PropertyId::Longhand(LonghandId::LineClamp);
assert_eq!(PropertyId::from_kebab_case("line-clamp"), line_clamp);
assert_eq!(PropertyId::from_camel_case("lineClamp"), line_clamp);
assert_eq!(
PropertyId::from_kebab_case("-webkit-line-clamp"),
line_clamp
);
assert_eq!(PropertyId::from_camel_case("WebkitLineClamp"), line_clamp);
assert_eq!(
PropertyId::from_kebab_case("-webkit-text-fill-color"),
PropertyId::Longhand(LonghandId::WebkitTextFillColor)
);
assert_eq!(
PropertyId::from_kebab_case("-webkit-text-stroke-color"),
PropertyId::Longhand(LonghandId::WebkitTextStrokeColor)
);
assert_eq!(
PropertyId::from_camel_case("WebkitTextStroke"),
PropertyId::Shorthand(ShorthandId::WebkitTextStroke)
);
assert_eq!(
PropertyId::from_kebab_case("-webkit-mask-image"),
PropertyId::Longhand(LonghandId::MaskImage)
);
}
#[test]
fn parse_webkit_line_clamp_matches_line_clamp() {
let mut line_clamp = Style::default();
line_clamp.append_block(parse_declarations("line-clamp", "2"));
let mut webkit_line_clamp = Style::default();
webkit_line_clamp.append_block(parse_declarations("-webkit-line-clamp", "2"));
assert_eq!(
webkit_line_clamp
.inherit(&ComputedStyle::default())
.line_clamp,
line_clamp.inherit(&ComputedStyle::default()).line_clamp
);
}
#[test]
fn property_id_accepts_legacy_gap_aliases() {
assert_eq!(
PropertyId::from_kebab_case("grid-gap"),
PropertyId::Shorthand(ShorthandId::Gap)
);
assert_eq!(
PropertyId::from_camel_case("gridGap"),
PropertyId::Shorthand(ShorthandId::Gap)
);
assert_eq!(
PropertyId::from_kebab_case("grid-row-gap"),
PropertyId::Longhand(LonghandId::RowGap)
);
assert_eq!(
PropertyId::from_camel_case("gridColumnGap"),
PropertyId::Longhand(LonghandId::ColumnGap)
);
}
#[test]
fn parse_style_declaration_supports_css_wide_keywords_for_longhands() {
let declarations = parse_declarations("color", "inherit");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::CssWideKeyword(
LonghandId::Color,
CssWideKeyword::Inherit,
)]
);
}
#[test]
fn parse_style_declaration_still_parses_normal_longhand_values() {
let declarations = parse_declarations("color", "#ff0000");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::color(ColorInput::Value(Color([
255, 0, 0, 255
])))]
);
}
#[test]
fn parse_style_declaration_parses_order_integer() {
let declarations = parse_declarations("order", "-2");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::order(Order(-2))]
);
}
#[test]
fn parse_text_fit_values() {
let cases = [
(
"none",
TextFit {
mode: TextFitMode::None,
target: TextFitTarget::Consistent,
limit: None,
},
),
(
"grow",
TextFit {
mode: TextFitMode::Grow,
target: TextFitTarget::Consistent,
limit: None,
},
),
(
"shrink",
TextFit {
mode: TextFitMode::Shrink,
target: TextFitTarget::Consistent,
limit: None,
},
),
(
"grow per-line 200%",
TextFit {
mode: TextFitMode::Grow,
target: TextFitTarget::PerLine,
limit: Some(2.0),
},
),
(
"shrink consistent 50%",
TextFit {
mode: TextFitMode::Shrink,
target: TextFitTarget::Consistent,
limit: Some(0.5),
},
),
];
for (input, expected) in cases {
let declarations = parse_declarations("text-fit", input);
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::text_fit(expected)]
);
}
}
#[test]
fn parse_text_fit_rejects_duplicate_components() {
for input in ["grow shrink", "grow per-line consistent", "grow 120% 140%"] {
assert!(parse_declarations_is_err("text-fit", input));
}
}
#[test]
fn parse_style_declaration_parses_z_index_values() {
let numeric = parse_declarations("z-index", "8");
assert_eq!(
numeric.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::z_index(ZIndex::Integer(8))]
);
let auto = parse_declarations("z-index", "auto");
assert_eq!(
auto.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::z_index(ZIndex::Auto)]
);
}
#[test]
fn parse_style_declaration_expands_shorthands_in_order() {
let declarations = parse_declarations("padding", "1px 2px");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::padding_top(Length::Px(1.0)),
&StyleDeclaration::padding_right(Length::Px(2.0)),
&StyleDeclaration::padding_bottom(Length::Px(1.0)),
&StyleDeclaration::padding_left(Length::Px(2.0)),
]
);
}
#[test]
fn parse_style_declaration_expands_flex_flow() {
let declarations = parse_declarations("flex-flow", "wrap row-reverse");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::flex_direction(FlexDirection::RowReverse),
&StyleDeclaration::flex_wrap(FlexWrap::Wrap),
]
);
}
#[test]
fn parse_style_declaration_expands_place_items() {
let declarations = parse_declarations("place-items", "center stretch");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::align_items(AlignItems::Center),
&StyleDeclaration::justify_items(AlignItems::Stretch),
]
);
}
#[test]
fn parse_style_declaration_expands_place_content() {
let declarations = parse_declarations("place-content", "space-between center");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::align_content(JustifyContent::SpaceBetween),
&StyleDeclaration::justify_content(JustifyContent::Center),
]
);
}
#[test]
fn parse_style_declaration_expands_place_self() {
let declarations = parse_declarations("place-self", "end stretch");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::align_self(AlignItems::End),
&StyleDeclaration::justify_self(AlignItems::Stretch),
]
);
}
#[test]
fn parse_align_items_safe_keywords() {
assert_eq!(
parse_declarations("align-items", "safe center")
.iter()
.collect::<Vec<_>>(),
vec![&StyleDeclaration::align_items(AlignItems::SafeCenter)]
);
assert_eq!(
parse_declarations("align-items", "unsafe flex-end")
.iter()
.collect::<Vec<_>>(),
vec![&StyleDeclaration::align_items(AlignItems::FlexEnd)]
);
assert!(parse_declarations_is_err("align-items", "safe stretch"));
assert!(parse_declarations_is_err("align-items", "safe"));
}
#[test]
fn parse_justify_content_safe_keywords() {
assert_eq!(
parse_declarations("justify-content", "safe flex-start")
.iter()
.collect::<Vec<_>>(),
vec![&StyleDeclaration::justify_content(
JustifyContent::SafeFlexStart
)]
);
assert!(parse_declarations_is_err(
"justify-content",
"safe space-between"
));
}
#[test]
fn parse_place_items_safe_keywords() {
assert_eq!(
parse_declarations("place-items", "safe start safe end")
.iter()
.collect::<Vec<_>>(),
vec![
&StyleDeclaration::align_items(AlignItems::SafeStart),
&StyleDeclaration::justify_items(AlignItems::SafeEnd),
]
);
}
#[test]
fn parse_style_declaration_expands_grid_row_shorthand() {
let declarations = parse_declarations("grid-row", "span 2 / 5");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::grid_row_start(GridPlacement::span(2)),
&StyleDeclaration::grid_row_end(GridPlacement::Line(5)),
]
);
}
#[test]
fn parse_style_declaration_expands_grid_area_shorthand() {
let declarations = parse_declarations("grid-area", "header / sidebar");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::grid_row_start(GridPlacement::Named("header".to_string())),
&StyleDeclaration::grid_column_start(GridPlacement::Named("sidebar".to_string())),
&StyleDeclaration::grid_row_end(GridPlacement::Named("header".to_string())),
&StyleDeclaration::grid_column_end(GridPlacement::Named("sidebar".to_string())),
]
);
}
#[test]
fn parse_style_declaration_supports_legacy_grid_gap_aliases() {
let row_gap = parse_declarations("grid-row-gap", "4px");
assert_eq!(
row_gap.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::row_gap(LengthDefaultsToZero::Px(4.0))]
);
let column_gap = parse_declarations("grid-column-gap", "3px");
assert_eq!(
column_gap.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::column_gap(LengthDefaultsToZero::Px(3.0))]
);
let gap = parse_declarations("grid-gap", "1px 2px");
assert_eq!(
gap.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::row_gap(LengthDefaultsToZero::Px(1.0)),
&StyleDeclaration::column_gap(LengthDefaultsToZero::Px(2.0)),
]
);
}
#[test]
fn parse_style_declaration_expands_border_side_shorthands() {
let border_top = parse_declarations("border-top", "2px solid red");
assert_eq!(
border_top.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::border_top_width(Length::Px(2.0)),
&StyleDeclaration::border_top_style(BorderStyle::Solid),
&StyleDeclaration::border_top_color(ColorInput::Value(Color([255, 0, 0, 255]))),
]
);
let border_left = parse_declarations("border-left", "solid #00ff00");
assert_eq!(
border_left.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::border_left_width(Length::default()),
&StyleDeclaration::border_left_style(BorderStyle::Solid),
&StyleDeclaration::border_left_color(ColorInput::Value(Color([0, 255, 0, 255]))),
]
);
}
#[test]
fn parse_style_declaration_expands_border_style_and_color_shorthands() {
let border_style = parse_declarations("border-style", "dashed hidden");
assert_eq!(
border_style.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::border_top_style(BorderStyle::Dashed),
&StyleDeclaration::border_right_style(BorderStyle::Hidden),
&StyleDeclaration::border_bottom_style(BorderStyle::Dashed),
&StyleDeclaration::border_left_style(BorderStyle::Hidden),
]
);
let border_color = parse_declarations("border-color", "red green blue yellow");
assert_eq!(
border_color.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::border_top_color(ColorInput::Value(Color([255, 0, 0, 255]))),
&StyleDeclaration::border_right_color(ColorInput::Value(Color([0, 128, 0, 255]))),
&StyleDeclaration::border_bottom_color(ColorInput::Value(Color([0, 0, 255, 255]))),
&StyleDeclaration::border_left_color(ColorInput::Value(Color([255, 255, 0, 255]))),
]
);
}
#[test]
fn parse_style_declaration_ignores_unknown_properties() {
let declarations = parse_declarations("not-a-real-property", "123");
assert!(declarations.iter().next().is_none());
}
#[test]
fn style_declaration_block_from_str_parses_multiple_declarations() {
let declarations = StyleDeclarationBlock::from_str("color: #ff0000; padding: 1px 2px;")
.expect("declarations should parse");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::color(ColorInput::Value(Color([255, 0, 0, 255]))),
&StyleDeclaration::padding_top(Length::Px(1.0)),
&StyleDeclaration::padding_right(Length::Px(2.0)),
&StyleDeclaration::padding_bottom(Length::Px(1.0)),
&StyleDeclaration::padding_left(Length::Px(2.0)),
]
);
}
#[test]
fn style_declaration_block_from_str_tracks_important_declarations() {
let declarations = StyleDeclarationBlock::from_str("color: inherit !important;")
.expect("declarations should parse");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![&StyleDeclaration::CssWideKeyword(
LonghandId::Color,
CssWideKeyword::Inherit,
)]
);
assert!(
declarations
.importance
.longhands
.contains(&LonghandId::Color)
);
}
#[test]
fn style_declaration_block_supports_reference_iteration() {
let declarations = parse_declarations("padding", "1px 2px");
assert_eq!(declarations.iter().count(), 4);
}
#[test]
fn style_declaration_block_collect_style_fetch_tasks_collects_url_images() {
let background_url = "https://placehold.co/80x80/22c55e/white";
let mask_url = "https://placehold.co/40x40/000000/white";
let declarations = StyleDeclarationBlock::from(style_with([
StyleDeclaration::background_image(Some(
[
BackgroundImage::Url(background_url.into()),
BackgroundImage::None,
]
.into(),
)),
StyleDeclaration::mask_image(Some([BackgroundImage::Url(mask_url.into())].into())),
]));
assert_eq!(
declarations.resource_urls().collect::<Vec<_>>(),
vec![background_url, mask_url]
);
}
#[test]
fn style_declaration_block_into_iter_yields_owned_values() {
let declarations = parse_declarations("padding", "1px 2px");
assert_eq!(
declarations.iter().collect::<Vec<_>>(),
vec![
&StyleDeclaration::padding_top(Length::Px(1.0)),
&StyleDeclaration::padding_right(Length::Px(2.0)),
&StyleDeclaration::padding_bottom(Length::Px(1.0)),
&StyleDeclaration::padding_left(Length::Px(2.0)),
]
);
}
#[test]
fn test_merge_from_text_decoration_longhands_clear_lower_priority_color() {
let mut preset_style = style_with([StyleDeclaration::text_decoration_color(ColorInput::Value(
Color([255, 0, 0, 255]),
))]);
let inline_style = style_with([
StyleDeclaration::text_decoration_line(Some(TextDecorationLines::UNDERLINE)),
StyleDeclaration::text_decoration_style(TextDecorationStyle::default()),
StyleDeclaration::text_decoration_color(ColorInput::default()),
StyleDeclaration::text_decoration_thickness(TextDecorationThickness::default()),
]);
preset_style.merge_from(inline_style);
let inherited = preset_style.inherit(&ComputedStyle::default());
assert_eq!(inherited.text_decoration_color, ColorInput::default());
assert_eq!(
inherited.text_decoration_line,
Some(TextDecorationLines::UNDERLINE)
);
}
#[test]
fn test_merge_from_background_longhands_clear_lower_priority_background_color() {
let mut preset_style = style_with([StyleDeclaration::background_color(ColorInput::Value(
Color([255, 0, 0, 255]),
))]);
let inline_style = style_with([
StyleDeclaration::background_image(Some([BackgroundImage::None].into())),
StyleDeclaration::background_position([BackgroundPosition::default()].into()),
StyleDeclaration::background_size([BackgroundSize::default()].into()),
StyleDeclaration::background_repeat([BackgroundRepeat::default()].into()),
StyleDeclaration::background_blend_mode([BlendMode::default()].into()),
StyleDeclaration::background_color(ColorInput::default()),
StyleDeclaration::background_clip(BackgroundClip::default()),
]);
preset_style.merge_from(inline_style);
let inherited = preset_style.inherit(&ComputedStyle::default());
assert_eq!(inherited.background_color, ColorInput::default());
}
#[test]
fn test_needs_offscreen_compositing_for_clip_path_and_mask_image() {
let mut style = ComputedStyle::default();
assert!(!style.needs_offscreen_compositing());
style.clip_path = BasicShape::from_str("inset(10px)").ok();
assert!(style.needs_offscreen_compositing());
style.clip_path = None;
style.mask_image = Some([BackgroundImage::Url("https://example.com/mask.png".into())].into());
assert!(style.needs_offscreen_compositing());
}
#[test]
fn test_is_z_index_applicable_matches_supported_scope() {
let mut style = ComputedStyle {
z_index: ZIndex::Integer(2),
..Default::default()
};
assert!(style.is_z_index_applicable(false));
style.position = Position::Absolute;
assert!(style.is_z_index_applicable(false));
style.position = Position::Relative;
assert!(style.is_z_index_applicable(false));
style.z_index = ZIndex::Auto;
assert!(!style.is_z_index_applicable(false));
}
#[test]
fn test_creates_stacking_context_from_z_index_scope() {
let mut style = ComputedStyle::default();
let sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 16.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
let border_box = Size {
width: 200.0,
height: 100.0,
};
style.z_index = ZIndex::Integer(1);
assert!(style.creates_stacking_context(border_box, &sizing, false));
style.position = Position::Absolute;
assert!(style.creates_stacking_context(border_box, &sizing, false));
}
#[test]
fn test_relative_position_participates_in_positioned_paint_bucket() {
let style = ComputedStyle {
position: Position::Relative,
..Default::default()
};
assert!(style.participates_in_positioned_paint_bucket(false));
}
#[test]
fn test_non_identity_transform_detection() {
let mut style = ComputedStyle::default();
let sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 16.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
let border_box = Size {
width: 200.0,
height: 100.0,
};
assert!(!style.has_non_identity_transform(border_box, &sizing));
style.transform = Some([Transform::Rotate(Angle::new(0.0))].into());
assert!(!style.has_non_identity_transform(border_box, &sizing));
style.transform = Some([Transform::Rotate(Angle::new(10.0))].into());
assert!(style.has_non_identity_transform(border_box, &sizing));
}
#[test]
fn test_transform_creates_stacking_context_without_offscreen_compositing() {
let mut style = ComputedStyle::default();
let sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 16.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
let border_box = Size {
width: 200.0,
height: 100.0,
};
style.transform = Some([Transform::Rotate(Angle::new(10.0))].into());
assert!(style.creates_stacking_context(border_box, &sizing, false));
assert!(!style.needs_offscreen_compositing());
}
#[test]
fn test_text_overflow_ellipsis_forces_single_line_clamp_on_nowrap() {
let style = ComputedStyle {
text_wrap_mode: TextWrapMode::NoWrap,
text_overflow: TextOverflow::Ellipsis,
..Default::default()
};
let (text_wrap_mode, line_clamp) = style.text_wrap_mode_and_line_clamp();
assert_eq!(text_wrap_mode, TextWrapMode::Wrap);
assert_eq!(
line_clamp,
Some(std::borrow::Cow::Owned(LineClamp {
count: 1,
ellipsis: Some("…".to_string()),
}))
);
}
#[test]
fn test_position_absolute_blockifies_inline_display() {
let mut style = style_with([
StyleDeclaration::display(Display::Inline),
StyleDeclaration::position(Position::Absolute),
])
.inherit(&ComputedStyle::default());
let sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 16.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
style.make_computed(&sizing);
assert_eq!(style.display, Display::Block);
}
#[test]
fn test_inherited_em_text_lengths_are_computed_once() {
let mut parent = style_with([
StyleDeclaration::font_size(Length::Em(2.0).into()),
StyleDeclaration::letter_spacing(Length::Em(1.0)),
StyleDeclaration::line_height(LineHeight::Length(Length::Em(1.5))),
])
.inherit(&ComputedStyle::default());
parent.make_computed(&SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 32.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
});
let inherited_child = Style::default().inherit(&parent);
let inherited_child_sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 32.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
let inherited_font_size = inherited_child
.font_size
.to_px(&inherited_child_sizing, inherited_child_sizing.font_size);
assert_eq!(inherited_font_size, 32.0);
let child_with_own_font_size =
style_with([StyleDeclaration::font_size(Length::Px(10.0).into())]).inherit(&parent);
let child_sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 10.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
let inherited_letter_spacing = child_with_own_font_size
.letter_spacing
.to_px(&child_sizing, child_sizing.font_size);
assert_eq!(inherited_letter_spacing, 32.0);
let inherited_line_height = match child_with_own_font_size.line_height {
LineHeight::Length(length) => length.to_px(&child_sizing, child_sizing.font_size),
_ => 0.0,
};
assert_eq!(inherited_line_height, 48.0);
}
#[test]
fn test_var_resolves_local_custom_property() {
let style = inherited_style_from_pairs(
[("--size", "24px"), ("width", "var(--size)")],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::Px(24.0));
}
#[test]
fn test_var_uses_fallback_when_missing() {
let style = inherited_style_from_pairs(
[("width", "var(--missing, 18px)")],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::Px(18.0));
}
#[test]
fn test_var_supports_nested_custom_properties() {
let style = inherited_style_from_pairs(
[
("--space-base", "12px"),
("--space", "var(--space-base)"),
("padding-left", "var(--space)"),
],
&ComputedStyle::default(),
);
assert_eq!(style.padding_left, Length::Px(12.0));
}
#[test]
fn test_var_resolves_custom_property_declared_later_on_same_element() {
let style = inherited_style_from_pairs(
[("width", "var(--card-width)"), ("--card-width", "24px")],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::Px(24.0));
}
#[test]
fn test_var_inherits_custom_properties_from_parent() {
let parent = inherited_style_from_pairs([("--card-width", "320px")], &ComputedStyle::default());
let child = inherited_style_from_pairs([("width", "var(--card-width)")], &parent);
assert_eq!(child.width, Length::Px(320.0));
}
#[test]
fn test_var_drops_invalid_declaration_without_fallback() {
let style = inherited_style_from_pairs([("width", "var(--missing)")], &ComputedStyle::default());
assert_eq!(style.width, Length::default());
}
#[test]
fn test_var_uses_fallback_for_cycles() {
let style = inherited_style_from_pairs(
[
("--a", "var(--b)"),
("--b", "var(--a)"),
("width", "var(--a, 14px)"),
],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::Px(14.0));
}
#[test]
fn test_var_resolves_inside_shorthand() {
let style = inherited_style_from_pairs(
[
("--block", "6px"),
("--inline", "10px"),
("padding", "var(--block) var(--inline)"),
],
&ComputedStyle::default(),
);
assert_eq!(style.padding_top, Length::Px(6.0));
assert_eq!(style.padding_right, Length::Px(10.0));
assert_eq!(style.padding_bottom, Length::Px(6.0));
assert_eq!(style.padding_left, Length::Px(10.0));
}
#[test]
fn test_var_rejects_non_custom_property_name() {
let style = inherited_style_from_pairs([("width", "var(size, 18px)")], &ComputedStyle::default());
assert_eq!(style.width, Length::default());
}
#[test]
fn test_var_allows_trailing_tokens_when_property_parser_is_loose() {
let style = inherited_style_from_pairs(
[("--size", "24px"), ("width", "var(--size) 10px")],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::Px(24.0));
}
#[test]
fn test_var_rejects_missing_separator_in_function() {
let style = inherited_style_from_pairs(
[("--size", "24px"), ("width", "var(--size 18px)")],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::default());
}
#[test]
fn test_var_supports_nested_fallback_chains() {
let style = inherited_style_from_pairs(
[
("--backup", "22px"),
("width", "var(--missing, var(--backup, 14px))"),
],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::Px(22.0));
}
#[test]
fn test_var_resolves_inside_nested_functions() {
let resolved = resolve_var("calc(var(--space) + 2px)", [("--space", "8px")]);
assert_eq!(resolved.as_deref(), Some("calc(8px + 2px)"));
}
#[test]
fn test_var_resolves_inside_nested_blocks() {
let resolved = resolve_var(
"(var(--x)) [var(--y)] {var(--z)}",
[("--x", "1px"), ("--y", "2px"), ("--z", "3px")],
);
assert_eq!(resolved.as_deref(), Some("(1px) [2px] {3px}"));
}
#[test]
fn test_var_drops_declaration_when_substitution_stays_invalid() {
let style = inherited_style_from_pairs(
[("--size", "red"), ("width", "var(--size)")],
&ComputedStyle::default(),
);
assert_eq!(style.width, Length::default());
}
#[test]
fn test_property_mask_iter_yields_only_set_bits_in_order() {
let mut mask = PropertyMask::new();
mask.insert(LonghandId::Width);
mask.insert(LonghandId::Color);
mask.insert(LonghandId::ALL[LonghandId::COUNT - 1]);
let collected: Vec<_> = mask.iter().collect();
let mut expected = [
LonghandId::Width,
LonghandId::Color,
LonghandId::ALL[LonghandId::COUNT - 1],
];
expected.sort_by_key(|id| id.index());
assert_eq!(collected, expected);
assert_eq!(PropertyMask::new().iter().count(), 0);
}
#[test]
fn test_var_defers_when_property_parser_accepts_a_prefix() {
let style = inherited_style_from_pairs(
[
("--onefifty", "150px"),
("background-position", "0 var(--onefifty)"),
],
&ComputedStyle::default(),
);
assert_eq!(
style.background_position.as_ref(),
[BackgroundPosition(SpacePair::from_pair(
PositionComponent::Length(Length::Px(0.0)),
PositionComponent::Length(Length::Px(150.0)),
))]
.as_slice(),
);
}
#[test]
fn test_border_radius_calc_infinity_parses_from_stylesheet_declaration() {
let style = inherited_style_from_pairs(
[("border-radius", "calc(infinity * 1px)")],
&ComputedStyle::default(),
);
let sizing = SizingContext {
viewport: Viewport::new((1200, 630)),
container_size: Size::NONE,
font_size: 16.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
};
let radius = style.border_top_left_radius.x.to_px(
&sizing,
sizing.viewport.size.width.unwrap_or_default() as f32,
);
assert_eq!(
radius,
i32::MAX as f32,
"parsed={:?}",
style.border_top_left_radius.x
);
}