//! DOM integration: build a taffy layout tree from a live [`DomTree`], run //! layout, and return border-box geometry keyed by [`NodeId`]. //! //! Phase 3. Text nodes do not yet contribute to size (no inline/text layout //! until the text/paint phase), so a leaf element with only text may have zero //! height. Block and flex structure, plus explicit sizes and box model, are //! correct. use std::collections::{HashMap, HashSet}; use std::sync::Arc; use obscura_dom::tree::{DomTree, NodeId}; use taffy::prelude::*; use crate::{to_taffy_style, Rect}; /// Text width for layout. With the `paint` feature this is exact (real glyph /// metrics from the embedded font, shared with rasterization). Without it /// (layout-only builds, e.g. for `getBoundingClientRect`), fall back to a /// standard average-character-width heuristic for proportional fonts so /// `obscura-render` compiles and gives reasonable geometry with its default /// (lightest-weight) feature set, matching RENDER.md's documented "layout /// (default)" mode. #[cfg(feature = "paint")] fn text_width( text: &str, size: f32, is_bold: bool, family: Option<&str>, letter_spacing: f32, ) -> f32 { crate::paint::measure_text(text, size, is_bold, family) + text.chars().filter(|c| !c.is_control()).count() as f32 * letter_spacing } #[cfg(not(feature = "paint"))] fn text_width( text: &str, size: f32, is_bold: bool, _family: Option<&str>, letter_spacing: f32, ) -> f32 { const AVG_CHAR_WIDTH_EM: f32 = 0.55; let chars = text.chars().filter(|c| !c.is_control()).count() as f32; let glyph_width = chars * size * AVG_CHAR_WIDTH_EM; (if is_bold { glyph_width * 1.08 } else { glyph_width }) + chars * letter_spacing } #[derive(Default)] struct NativeButtonIntrinsicContent { text: String, atomic_width: f32, } /// Collect the normal-flow content that contributes to an auto-sized /// ` "#, ); let laid = layout_dom(&tree, (800.0, 600.0)); let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()]; assert!( (rect("plain").width - rect("labels").width).abs() < 0.1, "hidden/out-of-flow labels enlarged the button: plain={:?}, labels={:?}", rect("plain"), rect("labels") ); assert!( (rect("icon").width - rect("icon-label").width).abs() < 0.1, "display:none icon label enlarged the button: icon={:?}, labelled={:?}", rect("icon"), rect("icon-label") ); assert!( (rect("icon").width - 32.0).abs() < 0.1, "the in-flow 16px SVG and horizontal padding must contribute: {:?}", rect("icon") ); } #[test] fn text_alignment_does_not_shrink_wrap_block_grid_and_flex_children() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1000.0, 300.0)); let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()]; let host = rect("host"); let grid = rect("grid"); let card_a = rect("card-a"); let card_b = rect("card-b"); let flex = rect("flex"); assert!((host.width - 900.0).abs() < 0.01, "{host:?}"); assert!((grid.width - 900.0).abs() < 0.01, "{grid:?}"); assert!( (card_a.width - 440.0).abs() < 0.01 && (card_b.width - 440.0).abs() < 0.01 && (card_b.x - card_a.x - 460.0).abs() < 0.01, "grid tracks escaped their containing block: {card_a:?} {card_b:?}" ); assert!((flex.width - 900.0).abs() < 0.01, "{flex:?}"); } #[test] fn sticky_normal_flow_excludes_own_pixel_and_percentage_translates() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (400.0, 240.0)); let sticky = laid.root_sticky_layout(&tree, (400.0, 240.0)); for name in ["pixel", "percent"] { let id = tree.get_element_by_id(name).unwrap(); let rect = laid.rects[&id]; let frame = sticky.frames.iter().find(|frame| frame.id == id).unwrap(); assert!( (frame.normal.x - rect.x).abs() < 0.01 && (frame.normal.y - rect.y).abs() < 0.01, "{name} own transform must not alter its sticky normal position: \ rect={rect:?} frame={frame:?}" ); } } #[test] fn individual_translate_percentage_uses_own_border_box() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (800.0, 600.0)); let card = tree.get_element_by_id("card").unwrap(); let rect = laid.rects[&card]; let translated = laid.translates[&card]; assert!( (rect.y - 206.0).abs() < 0.01, "absolute top:50% must use the containing block: {rect:?}" ); assert!( translated.0.abs() < 0.01 && (translated.1 + 140.0).abs() < 0.01, "translate percentage must use the card's own 280px border box: {translated:?}" ); assert!( laid.styles[&card].establishes_positioning_containing_block(), "an individual translate creates a containing block like transform" ); } #[test] fn sticky_normal_flow_retains_transformed_ancestor_coordinates() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (400.0, 240.0)); let id = tree.get_element_by_id("sticky").unwrap(); let rect = laid.rects[&id]; let sticky = laid.root_sticky_layout(&tree, (400.0, 240.0)); let frame = sticky.frames.iter().find(|frame| frame.id == id).unwrap(); assert!( (frame.normal.x - (rect.x + 25.0)).abs() < 0.01 && (frame.normal.y - (rect.y + 30.0)).abs() < 0.01, "ancestor transform must remain in sticky constraint coordinates: \ rect={rect:?} frame={frame:?}" ); } #[test] fn container_query_uses_final_content_box_geometry() { let tree = parse_html( r#"
"#, ); let (laid, telemetry) = layout_dom_with_web_fonts_measured(&tree, (1280.0, 720.0), &HashMap::new(), &[]); let container = tree.get_element_by_id("container").unwrap(); let target = tree.get_element_by_id("target").unwrap(); assert!((laid.rects[&container].width - 500.0).abs() < 0.1); assert_eq!(laid.styles[&target].width, crate::Dimension::Px(44.0)); assert_eq!(telemetry.passes, 2); assert_eq!( telemetry.termination, ContainerLayoutTermination::GeometryStable ); } #[test] fn container_query_named_and_unnamed_lookup_choose_different_ancestors() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let target = tree.get_element_by_id("target").unwrap(); assert_eq!(laid.styles[&target].width, crate::Dimension::Px(11.0)); assert_eq!(laid.styles[&target].height, crate::Dimension::Px(1.0)); } #[test] fn nested_container_queries_select_independent_ancestors() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let target = tree.get_element_by_id("target").unwrap(); assert_eq!(laid.styles[&target].width, crate::Dimension::Px(19.0)); } #[test] fn container_query_pseudo_can_select_its_originating_element() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let container = tree.get_element_by_id("container").unwrap(); assert_eq!( laid.styles[&container] .before_pseudo .as_ref() .and_then(|pseudo| pseudo.before_content.as_deref()), Some("active") ); } #[test] fn container_type_zeroes_shrink_to_fit_inline_intrinsic_widths() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 300.0)); for name in ["inline-block", "inline-flex", "inline-grid"] { let id = tree.get_element_by_id(name).unwrap(); let rect = laid.rects[&id]; assert!( rect.width.abs() < 0.01, "{name} descendants leaked into contained intrinsic width: {rect:?}" ); assert!( rect.height >= 29.9, "inline-size containment must retain content-driven block size: {rect:?}" ); } } #[test] fn inline_outer_boxes_shrink_wrap_in_only_child_and_text_flows() { let tree = parse_html( r#"
A
B
A
B
A
B
"#, ); let laid = layout_dom(&tree, (700.0, 300.0)); for name in ["ib", "if", "ig", "ibt", "ift", "igt"] { let rect = laid.rects[&tree.get_element_by_id(name).unwrap()]; assert!( (rect.width - 400.0).abs() < 0.01, "{name} did not shrink-wrap its 400px child: {rect:?}" ); } } #[test] fn inline_flex_and_grid_preserve_percentage_and_box_edge_geometry() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (800.0, 300.0)); let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()]; for name in ["flex-percent", "grid-percent"] { let item = rect(name); assert!( (item.width - 350.0).abs() < 0.01 && (item.height - 20.0).abs() < 0.01, "{name}: {item:?}" ); } for name in ["flex-edges", "grid-edges"] { let item = rect(name); assert!( (item.x - 7.0).abs() < 0.01 && (item.width - 150.0).abs() < 0.01 && (item.height - 50.0).abs() < 0.01, "{name}: {item:?}" ); } } #[test] fn inline_flex_and_grid_resolve_percentage_min_max_against_containing_block() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (800.0, 300.0)); for name in ["flex-max", "grid-max", "flex-min", "grid-min"] { let item = laid.rects[&tree.get_element_by_id(name).unwrap()]; assert!( (item.width - 350.0).abs() < 0.01, "{name} resolved its percentage constraint against the wrong box: {item:?}" ); } } #[test] fn inline_atomic_vertical_align_controls_cross_axis_placement() { let tree = parse_html( r#"
x
x
"#, ); let laid = layout_dom(&tree, (800.0, 200.0)); let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()]; let bottom_short = rect("bottom-short"); let bottom_tall = rect("bottom-tall"); assert!( (bottom_short.y + bottom_short.height - bottom_tall.y - bottom_tall.height).abs() < 0.01, "bottom-aligned atoms did not share a line bottom: {bottom_short:?} {bottom_tall:?}" ); let middle_short = rect("middle-short"); let middle_tall = rect("middle-tall"); assert!( (middle_short.y + middle_short.height / 2.0 - middle_tall.y - middle_tall.height / 2.0) .abs() < 0.01, "middle-aligned atoms did not share a line center: {middle_short:?} {middle_tall:?}" ); } #[test] fn blockified_inline_flex_and_grid_items_retain_inner_layout() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (700.0, 300.0)); let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()]; for (container, first, second) in [("if", "ifa", "ifb"), ("ig", "iga", "igb")] { let container = rect(container); let first = rect(first); let second = rect(second); assert!((container.width - 200.0).abs() < 0.01, "{container:?}"); assert!( (first.y - second.y).abs() < 0.01 && (second.x - first.x - 100.0).abs() < 0.01, "inner layout was destroyed: first={first:?} second={second:?}" ); } } #[test] fn overflowing_grid_item_auto_margins_use_safe_logical_start() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (800.0, 300.0)); let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()]; for (grid_id, item_id, expected_x) in [ ("ltr-center-grid", "ltr-center", 0.0), ("ltr-end-grid", "ltr-end", 0.0), ] { let grid = rect(grid_id); let item = rect(item_id); assert!( (item.width - 600.0).abs() < 0.01, "{item_id} lost its min-content floor: {item:?}" ); assert!( (item.x - grid.x - expected_x).abs() < 0.01, "{item_id} did not use safe logical-start overflow: grid={grid:?} item={item:?}" ); } for (grid_id, item_id, expected_x) in [ ("absolute-center-grid", "absolute-center", -150.0), ("absolute-end-grid", "absolute-end", -300.0), ] { let grid = rect(grid_id); let item = rect(item_id); assert!( (item.x - grid.x - expected_x).abs() < 0.01, "{item_id} incorrectly used the in-flow auto-margin fallback: grid={grid:?} item={item:?}" ); } } #[test] fn root_and_body_inline_outer_geometry_matches_blockification_rules() { for display in ["inline-block", "inline-flex", "inline-grid"] { let tree = parse_html(&format!( r#"
"# )); let laid = layout_dom(&tree, (800.0, 300.0)); let body = tree .descendants(tree.document()) .into_iter() .find(|id| { tree.get_node(*id).is_some_and(|node| { node.as_element() .is_some_and(|element| element.local.as_ref() == "body") }) }) .unwrap(); assert!( (laid.rects[&body].width - 400.0).abs() < 0.01, "body {display}: {:?}", laid.rects[&body] ); let tree = parse_html(&format!( r#"
"# )); let laid = layout_dom(&tree, (800.0, 300.0)); let root = tree .descendants(tree.document()) .into_iter() .find(|id| { tree.get_node(*id).is_some_and(|node| { node.as_element() .is_some_and(|element| element.local.as_ref() == "html") }) }) .unwrap(); assert!( (laid.rects[&root].width - 800.0).abs() < 0.01, "root {display}: {:?}", laid.rects[&root] ); } } #[test] fn viewport_overflow_clip_does_not_truncate_root_scrolling_overflow() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (900.0, 1000.0)); let main = laid.rects[&tree.get_element_by_id("main").unwrap()]; let content = laid.scrolling_content_size(&tree, (900.0, 1000.0)); assert!((main.height - 5048.0).abs() < 0.01, "{main:?}"); assert_eq!(content, (900.0, 5048.0)); } #[test] fn propagated_body_overflow_is_visible_to_layout_and_does_not_create_a_bfc() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (100.0, 60.0)); let body = tree .descendants(tree.document()) .into_iter() .find(|id| { tree.get_node(*id).is_some_and(|node| { node.as_element() .is_some_and(|element| element.local.as_ref() == "body") }) }) .unwrap(); let style = &laid.styles[&body]; assert!(style.overflow_propagated_to_viewport); assert!(!establishes_block_formatting_context(style)); let taffy = crate::to_taffy_style(style); assert_eq!(taffy.overflow.x, taffy::style::Overflow::Visible); assert_eq!(taffy.overflow.y, taffy::style::Overflow::Visible); } #[test] fn html_owned_overflow_keeps_body_clip_in_root_scrolling_overflow() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (80.0, 40.0)); assert_eq!( laid.scrolling_content_size(&tree, (80.0, 40.0)), (100.0, 50.0) ); } #[test] fn descendant_overflow_clip_still_bounds_root_scrolling_overflow() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (900.0, 1000.0)); assert_eq!( laid.scrolling_content_size(&tree, (900.0, 1000.0)), (900.0, 1000.0) ); } #[test] fn size_container_auto_block_size_ignores_descendants() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 300.0)); let inline_only = laid.rects[&tree.get_element_by_id("inline-only").unwrap()]; let both = laid.rects[&tree.get_element_by_id("both").unwrap()]; assert!((inline_only.height - 80.0).abs() < 0.01, "{inline_only:?}"); assert!( both.height.abs() < 0.01, "size containment must use the zero contain-intrinsic block size: {both:?}" ); } #[test] fn container_type_establishes_a_float_containing_formatting_context() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 200.0)); let container = laid.rects[&tree.get_element_by_id("container").unwrap()]; assert!( (container.height - 40.0).abs() < 0.01, "query container must contain its descendant float: {container:?}" ); } #[test] fn ineligible_nearest_inline_container_does_not_fall_through() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 200.0)); let target = tree.get_element_by_id("target").unwrap(); assert_eq!( laid.styles[&target].width, crate::Dimension::Px(1.0), "the unavailable nearest query axis must be unknown, not select the outer container" ); } #[test] fn ineligible_nearest_display_contents_container_does_not_fall_through() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 200.0)); let target = tree.get_element_by_id("target").unwrap(); assert_eq!( laid.styles[&target].width, crate::Dimension::Px(1.0), "a named display:contents container must remain the nearest match with unavailable axes" ); } #[test] fn ineligible_nearest_table_container_does_not_fall_through() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 200.0)); let inner = tree.get_element_by_id("inner").unwrap(); let target = tree.get_element_by_id("target").unwrap(); assert!( laid.styles[&inner].is_table_box, "the internal block approximation lost authored table provenance" ); assert_eq!( laid.styles[&target].width, crate::Dimension::Px(1.0), "an unavailable nearest table query axis must not fall through to the outer container" ); assert!( (laid.rects[&inner].height - 80.0).abs() < 0.01, "container-type must not apply size containment to an authored table box: {:?}", laid.rects[&inner] ); } #[test] fn container_type_does_not_size_contain_internal_table_cell() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (600.0, 200.0)); let cell = tree.get_element_by_id("cell").unwrap(); assert!( laid.styles[&cell].internal_flex_container, "the test must exercise the internal table-cell representation" ); assert!( laid.rects[&cell].height >= 79.9, "container-type must not zero an internal table cell: {:?}", laid.rects[&cell] ); } #[test] fn formerly_oscillating_inline_container_settles_consistently() { let tree = parse_html( r#"
"#, ); let (laid, telemetry) = layout_dom_with_web_fonts_measured(&tree, (600.0, 200.0), &HashMap::new(), &[]); let container = laid.rects[&tree.get_element_by_id("container").unwrap()]; let child = tree.get_element_by_id("child").unwrap(); assert!(container.width.abs() < 0.01, "{container:?}"); assert_eq!(laid.styles[&child].width, crate::Dimension::Px(200.0)); assert!(matches!( telemetry.termination, ContainerLayoutTermination::GeometryStable | ContainerLayoutTermination::SignatureStable )); } #[test] fn pass_cap_uses_visible_conservative_fallback_without_panicking() { let tree = parse_html( r#"
"#, ); let (laid, telemetry) = layout_dom_with_web_fonts_pass_limit( &tree, (600.0, 200.0), &HashMap::new(), &[], Some(2), None, None, &[], ); let target = tree.get_element_by_id("target").unwrap(); assert_eq!(laid.styles[&target].width, crate::Dimension::Px(1.0)); assert_eq!( telemetry.termination, ContainerLayoutTermination::PassCapFallback ); assert_eq!(telemetry.passes, 3); } #[test] fn ten_level_container_activation_propagates_without_fixed_pass_truncation() { let mut css = String::from("#c0{container:c0/inline-size;width:100px}#target{width:1px}"); for level in 0..10 { css.push_str(&format!( "@container c{level} (min-width:100px){{#c{}{{container:c{}/inline-size;width:100px}}}}", level + 1, level + 1 )); } css.push_str("@container c10 (min-width:100px){#target{width:77px}}"); let mut body = String::new(); for level in 0..=10 { body.push_str(&format!("
")); } body.push_str("
"); for _ in 0..=10 { body.push_str("
"); } let tree = parse_html(&format!("{body}")); let (laid, telemetry) = layout_dom_with_web_fonts_measured(&tree, (800.0, 600.0), &HashMap::new(), &[]); let target = tree.get_element_by_id("target").unwrap(); assert_eq!(laid.styles[&target].width, crate::Dimension::Px(77.0)); assert!( telemetry.passes > 8, "deep propagation unexpectedly truncated: {telemetry:?}" ); assert!(!matches!( telemetry.termination, ContainerLayoutTermination::PassCapFallback | ContainerLayoutTermination::OscillationFallback )); } #[test] fn layout_without_container_queries_keeps_one_pass_fast_path() { let tree = parse_html( r#"
"#, ); let (_, telemetry) = layout_dom_with_web_fonts_measured(&tree, (800.0, 600.0), &HashMap::new(), &[]); assert_eq!( telemetry, ContainerLayoutTelemetry { passes: 1, termination: ContainerLayoutTermination::NoQueries, query: crate::css::ContainerQueryStats::default(), retained_reused: 0, retained_fresh: 0, retained_fallback: 0, } ); } #[test] fn dormant_container_queries_without_a_container_keep_one_pass_fast_path() { let tree = parse_html( r#"
"#, ); let (laid, telemetry) = layout_dom_with_web_fonts_measured(&tree, (800.0, 600.0), &HashMap::new(), &[]); let target = tree.get_element_by_id("target").unwrap(); assert_eq!(laid.styles[&target].width, crate::Dimension::Px(25.0)); assert_eq!( telemetry, ContainerLayoutTelemetry { passes: 1, termination: ContainerLayoutTermination::NoContainers, query: crate::css::ContainerQueryStats::default(), retained_reused: 0, retained_fresh: 0, retained_fallback: 0, } ); } #[test] fn retained_attribute_styles_match_forced_full_with_dormant_container_rules() { let initial_html = r#"
x
"#; let final_html = initial_html .replace("data-theme=light", "data-theme=dark") .replace("data-open=false", "data-open=true") .replace("data-double=false", "data-double=true"); let tree = parse_html(initial_html); let mut cache = crate::css::StylesheetCache::default(); let mut initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (300.0, 200.0), &HashMap::new(), &[], &mut cache, ); let theme = tree.get_element_by_id("theme").unwrap(); let toggle = tree.get_element_by_id("toggle").unwrap(); let counter_change = tree.get_element_by_id("counter-change").unwrap(); tree.with_node_mut(theme, |node| node.set_attribute("data-theme", "dark".into())); tree.with_node_mut(toggle, |node| node.set_attribute("data-open", "true".into())); tree.with_node_mut(counter_change, |node| { node.set_attribute("data-double", "true".into()) }); let mutations = vec![ AttributeStyleMutation { node: theme, name: "data-theme".into(), old_value: Some("light".into()), new_value: Some("dark".into()), } .into(), AttributeStyleMutation { node: toggle, name: "data-open".into(), old_value: Some("false".into()), new_value: Some("true".into()), } .into(), AttributeStyleMutation { node: counter_change, name: "data-double".into(), old_value: Some("false".into()), new_value: Some("true".into()), } .into(), ]; let retained = RetainedStyleMaps { styles: std::mem::take(&mut initial.styles), custom_properties: std::mem::take(&mut initial.custom_properties), }; let (incremental, telemetry) = layout_dom_with_web_fonts_pass_limit( &tree, (300.0, 200.0), &HashMap::new(), &[], None, Some(&mut cache), Some(retained), &mutations, ); let final_tree = parse_html(&final_html); let full = layout_dom(&final_tree, (300.0, 200.0)); assert_eq!(telemetry.termination, ContainerLayoutTermination::NoContainers); assert!(telemetry.retained_reused > 0, "clean branch must be reused"); assert!(telemetry.retained_fresh > 0, "dirty subtrees must be fresh"); assert_eq!(telemetry.retained_fallback, 0); for id in [ "card", "child", "peer", "counter-one", "counter-change", "counter-after", "clean", "control", "inherit", ] { let incremental_id = tree.get_element_by_id(id).unwrap(); let full_id = final_tree.get_element_by_id(id).unwrap(); assert_eq!( incremental.rects.get(&incremental_id), full.rects.get(&full_id), "{id} rect" ); assert_eq!( incremental.styles[&incremental_id].width, full.styles[&full_id].width, "{id} width" ); assert_eq!( incremental.styles[&incremental_id].color, full.styles[&full_id].color, "{id} color" ); assert_eq!( incremental.styles[&incremental_id].before_content, full.styles[&full_id].before_content, "{id} generated content" ); assert_eq!( incremental.custom_properties[&incremental_id], full.custom_properties[&full_id], "{id} custom properties" ); } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum RetainedDifferentialExpectation { Incremental, ReuseAll, ConservativeFallback, } struct RetainedDifferentialCase { name: &'static str, css: &'static str, target: &'static str, attribute: &'static str, new_value: &'static str, expectation: RetainedDifferentialExpectation, } /// Compare every computed-style field through its Debug representation. /// `LayoutStyle` intentionally does not implement PartialEq because it is /// a large, evolving renderer-internal type; keeping this check centralized /// means new fields automatically enter the retained-vs-full oracle. fn assert_computed_styles_match( case: &str, incremental: &DomLayout, full: &DomLayout, ) { assert_eq!( incremental.styles.keys().collect::>(), full.styles.keys().collect::>(), "{case}: computed-style node set" ); for (node, incremental_style) in &incremental.styles { assert_eq!( format!("{incremental_style:#?}"), format!("{:#?}", full.styles[node]), "{case}: computed style for {node:?}" ); } } fn run_retained_differential_case(case: &RetainedDifferentialCase) { let html = format!( r#"
"#, case.css ); let tree = parse_html(&html); let mut cache = crate::css::StylesheetCache::default(); let mut initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (320.0, 240.0), &HashMap::new(), &[], &mut cache, ); let target = tree.get_element_by_id(case.target).unwrap(); let old_value = tree .get_node(target) .and_then(|node| node.get_attribute(case.attribute).map(str::to_owned)); tree.with_node_mut(target, |node| { node.set_attribute(case.attribute, case.new_value.into()) }); let retained = RetainedStyleMaps { styles: std::mem::take(&mut initial.styles), custom_properties: std::mem::take(&mut initial.custom_properties), }; let (incremental, telemetry) = layout_dom_with_web_fonts_pass_limit( &tree, (320.0, 240.0), &HashMap::new(), &[], None, Some(&mut cache), Some(retained), &[AttributeStyleMutation { node: target, name: case.attribute.into(), old_value, new_value: Some(case.new_value.into()), } .into()], ); let full = layout_dom(&tree, (320.0, 240.0)); match case.expectation { RetainedDifferentialExpectation::Incremental => { assert_eq!(telemetry.retained_fallback, 0, "{}", case.name); assert!(telemetry.retained_fresh > 0, "{}", case.name); assert!(telemetry.retained_reused > 0, "{}", case.name); } RetainedDifferentialExpectation::ReuseAll => { assert_eq!(telemetry.retained_fallback, 0, "{}", case.name); assert_eq!(telemetry.retained_fresh, 0, "{}", case.name); assert!(telemetry.retained_reused > 0, "{}", case.name); } RetainedDifferentialExpectation::ConservativeFallback => { assert_eq!(telemetry.retained_fallback, 1, "{}", case.name); assert_eq!(telemetry.retained_reused, 0, "{}", case.name); } } assert_computed_styles_match(case.name, &incremental, &full); assert_eq!(incremental.rects, full.rects, "{}: border boxes", case.name); assert_eq!( incremental.inline_fragments, full.inline_fragments, "{}: inline fragments", case.name ); assert_eq!(incremental.text_runs, full.text_runs, "{}: text runs", case.name); assert_eq!( incremental.custom_properties, full.custom_properties, "{}: custom properties", case.name ); } #[test] fn retained_attribute_styles_match_forced_full_selector_matrix() { use RetainedDifferentialExpectation::{ ConservativeFallback, Incremental, ReuseAll, }; let cases = [ RetainedDifferentialCase { name: "self attribute selector", css: ".subject[data-state=on]{width:41px;height:7px;color:#123456}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "child and descendant selectors", css: ".subject[data-state=on]>.child .grand{width:43px;height:9px}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "ancestor selector", css: ".scope[data-mode=on] .grand{width:47px;height:11px}", target: "scope", attribute: "data-mode", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "adjacent sibling selector", css: ".subject[data-state=on]+.panel{width:53px;height:13px}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "general sibling selector", css: ".subject[data-state=on]~.panel{width:59px;height:15px}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "inheritance and custom property", css: ".scope[data-mode=on]{--leaf-width:61px;color:#234567;font-size:20px}.leaf{width:var(--leaf-width,17px);height:1em;color:inherit}", target: "scope", attribute: "data-mode", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "generated before and after content", css: ".subject[data-state=on]::before{content:'before';display:block;width:5px}.subject[data-state=on]::after{content:'after';display:block;width:7px}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "declaration attr dependency", css: ".subject::before{content:attr(data-state);display:block;width:11px}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "inline style and inherited custom property", css: ".subject .grand{width:var(--leaf-width,17px);color:inherit}", target: "subject", attribute: "style", new_value: "width:113px;height:29px;--leaf-width:37px;color:#345678", expectation: Incremental, }, RetainedDifferentialCase { name: "inline style attribute selector reaches sibling", css: ".subject[style]+.panel{width:127px;height:31px}", target: "subject", attribute: "style", new_value: "height:23px", expectation: Incremental, }, RetainedDifferentialCase { name: "counter propagation through retained siblings", css: ".scope{counter-reset:item}.subject,.panel{counter-increment:item}.subject[data-state=on]{counter-increment:item 3}.subject::before,.panel::before{content:counter(item)}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "class replacement", css: ".subject.state-on .grand{width:67px;height:17px}", target: "subject", attribute: "class", new_value: "subject state-on", expectation: Incremental, }, RetainedDifferentialCase { name: "id replacement", css: "#subject-on .grand{width:71px;height:19px}", target: "subject", attribute: "id", new_value: "subject-on", expectation: Incremental, }, RetainedDifferentialCase { name: "selector-list pseudo", css: ".scope:is([data-mode=on],.alternate) .grand{width:73px;height:21px}", target: "scope", attribute: "data-mode", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "negation pseudo", css: ".subject:not([data-state=off]){width:79px;height:23px}", target: "subject", attribute: "data-state", new_value: "on", expectation: Incremental, }, RetainedDifferentialCase { name: "mixed sibling descendant traversal", css: ".subject[data-state=on]~.panel .leaf{width:83px;height:25px}", target: "subject", attribute: "data-state", new_value: "on", expectation: ConservativeFallback, }, RetainedDifferentialCase { name: "relative selector ancestor traversal", css: ".scope:has(.subject[data-state=on]){width:89px}", target: "subject", attribute: "data-state", new_value: "on", expectation: ConservativeFallback, }, RetainedDifferentialCase { name: "nth of selector structural traversal", css: ".subject:nth-child(1 of [data-state=on]){width:97px}", target: "subject", attribute: "data-state", new_value: "on", expectation: ConservativeFallback, }, RetainedDifferentialCase { name: "unreferenced aria mutation", css: ".subject{width:101px;height:27px}", target: "subject", attribute: "aria-expanded", new_value: "true", expectation: ReuseAll, }, ]; for case in &cases { run_retained_differential_case(case); } } fn finish_retained_tree_case( name: &str, tree: &DomTree, cache: &mut crate::css::StylesheetCache, mut initial: DomLayout, mutation: TreeStyleMutation, expectation: RetainedDifferentialExpectation, ) -> DomLayout { let retained = RetainedStyleMaps { styles: std::mem::take(&mut initial.styles), custom_properties: std::mem::take(&mut initial.custom_properties), }; let (incremental, telemetry) = layout_dom_with_web_fonts_pass_limit( tree, (360.0, 260.0), &HashMap::new(), &[], None, Some(cache), Some(retained), &[mutation.into()], ); let full = layout_dom(tree, (360.0, 260.0)); match expectation { RetainedDifferentialExpectation::Incremental => { assert_eq!(telemetry.retained_fallback, 0, "{name}"); assert!(telemetry.retained_fresh > 0, "{name}"); assert!(telemetry.retained_reused > 0, "{name}"); } RetainedDifferentialExpectation::ReuseAll => { assert_eq!(telemetry.retained_fallback, 0, "{name}"); assert_eq!(telemetry.retained_fresh, 0, "{name}"); assert!(telemetry.retained_reused > 0, "{name}"); } RetainedDifferentialExpectation::ConservativeFallback => { assert_eq!(telemetry.retained_fallback, 1, "{name}"); assert_eq!(telemetry.retained_reused, 0, "{name}"); } } assert_computed_styles_match(name, &incremental, &full); assert_eq!(incremental.rects, full.rects, "{name}: border boxes"); assert_eq!( incremental.inline_fragments, full.inline_fragments, "{name}: inline fragments" ); assert_eq!(incremental.text_runs, full.text_runs, "{name}: text runs"); assert_eq!( incremental.custom_properties, full.custom_properties, "{name}: custom properties" ); incremental } #[test] fn retained_tree_styles_match_forced_full_mutation_matrix() { use RetainedDifferentialExpectation::{ConservativeFallback, Incremental}; // A fresh insertion must cascade the inserted subtree and every // structurally affected sibling while retaining an unrelated branch. let tree = parse_html( r#"
"#, ); let list = tree.get_element_by_id("list").unwrap(); let insert = tree.get_element_by_id("insert").unwrap(); let last = tree.get_element_by_id("last").unwrap(); tree.remove_child(insert); let mut cache = crate::css::StylesheetCache::default(); let initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (360.0, 260.0), &HashMap::new(), &[], &mut cache, ); tree.insert_before(last, insert); finish_retained_tree_case( "fresh insert with nth, counters, and pseudos", &tree, &mut cache, initial, TreeStyleMutation::Insert { node: insert, old_parent: None, new_parent: list, }, Incremental, ); // Removing a subtree must purge its now-detached style/custom-property // entries instead of growing the retained maps forever. let tree = parse_html( r#"
"#, ); let rows = tree.get_element_by_id("rows").unwrap(); let removed = tree.get_element_by_id("removed").unwrap(); let removed_child = tree.get_element_by_id("removed-child").unwrap(); let mut cache = crate::css::StylesheetCache::default(); let initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (360.0, 260.0), &HashMap::new(), &[], &mut cache, ); tree.remove_child(removed); let incremental = finish_retained_tree_case( "remove and purge detached subtree", &tree, &mut cache, initial, TreeStyleMutation::Remove { node: removed, old_parent: rows, }, Incremental, ); assert!(!incremental.styles.contains_key(&removed)); assert!(!incremental.styles.contains_key(&removed_child)); assert!(!incremental.custom_properties.contains_key(&removed)); assert!(!incremental.custom_properties.contains_key(&removed_child)); // Character-data changes keep selector matching local but must rebuild // text shaping and parent geometry. let tree = parse_html( r#"
a
"#, ); let label = tree.get_element_by_id("label").unwrap(); let text = tree.children(label)[0]; let mut cache = crate::css::StylesheetCache::default(); let initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (360.0, 260.0), &HashMap::new(), &[], &mut cache, ); tree.with_node_mut(text, |node| { if let obscura_dom::tree::NodeData::Text { contents } = &mut node.data { *contents = "a substantially longer replacement".into(); } }); finish_retained_tree_case( "text replacement", &tree, &mut cache, initial, TreeStyleMutation::Text { node: text, parent: Some(label), }, Incremental, ); // Keyed :has() rules do not poison unrelated mutations, but a matching // inserted key still takes the correctness-first full path. for (name, inserted_class, expectation) in [ ("unrelated keyed has insertion", "other", Incremental), ("matching keyed has insertion", "signal", ConservativeFallback), ] { let html = format!( r#"
"# ); let tree = parse_html(&html); let target = tree.get_element_by_id("target").unwrap(); let insert = tree.get_element_by_id("insert").unwrap(); tree.remove_child(insert); let mut cache = crate::css::StylesheetCache::default(); let initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (360.0, 260.0), &HashMap::new(), &[], &mut cache, ); tree.append_child(target, insert); finish_retained_tree_case( name, &tree, &mut cache, initial, TreeStyleMutation::Insert { node: insert, old_parent: None, new_parent: target, }, expectation, ); } for (name, inserted_tag, expectation) in [ ("unrelated keyed tag has insertion", "i", Incremental), ("matching keyed tag has insertion", "span", ConservativeFallback), ] { let html = format!( r#"
<{inserted_tag} id="insert">
"# ); let tree = parse_html(&html); let target = tree.get_element_by_id("host").unwrap(); let insert = tree.get_element_by_id("insert").unwrap(); tree.remove_child(insert); let mut cache = crate::css::StylesheetCache::default(); let initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (360.0, 260.0), &HashMap::new(), &[], &mut cache, ); tree.append_child(target, insert); finish_retained_tree_case( name, &tree, &mut cache, initial, TreeStyleMutation::Insert { node: insert, old_parent: None, new_parent: target, }, expectation, ); } // Style text changes alter the cache key and must never reuse styles. let tree = parse_html( r#"
"#, ); let style = tree.get_element_by_id("sheet").unwrap(); let text = tree.children(style)[0]; let mut cache = crate::css::StylesheetCache::default(); let initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (360.0, 260.0), &HashMap::new(), &[], &mut cache, ); tree.with_node_mut(text, |node| { if let obscura_dom::tree::NodeData::Text { contents } = &mut node.data { *contents = ".subject{width:140px}".into(); } }); finish_retained_tree_case( "style text fallback", &tree, &mut cache, initial, TreeStyleMutation::Text { node: text, parent: Some(style), }, ConservativeFallback, ); } #[test] fn retained_tree_invalidation_keeps_large_unrelated_branch_clean() { let mut clean = String::new(); for index in 0..2_000 { clean.push_str(&format!("")); } let tree = parse_html(&format!( r#"
"# )); let list = tree.get_element_by_id("list").unwrap(); let insert = tree.get_element_by_id("insert").unwrap(); tree.remove_child(insert); let mut cache = crate::css::StylesheetCache::default(); let mut initial = layout_dom_with_web_fonts_and_stylesheet_cache( &tree, (800.0, 600.0), &HashMap::new(), &[], &mut cache, ); tree.append_child(list, insert); let retained = RetainedStyleMaps { styles: std::mem::take(&mut initial.styles), custom_properties: std::mem::take(&mut initial.custom_properties), }; let (incremental, telemetry) = layout_dom_with_web_fonts_pass_limit( &tree, (800.0, 600.0), &HashMap::new(), &[], None, Some(&mut cache), Some(retained), &[TreeStyleMutation::Insert { node: insert, old_parent: None, new_parent: list, } .into()], ); let full = layout_dom(&tree, (800.0, 600.0)); assert_computed_styles_match("large unrelated branch", &incremental, &full); assert_eq!(incremental.rects, full.rects); assert_eq!(telemetry.retained_fallback, 0); assert!(telemetry.retained_reused >= 2_000, "{telemetry:?}"); assert!(telemetry.retained_fresh < 16, "{telemetry:?}"); } #[test] fn container_convergence_classifies_self_consistent_stability() { assert_eq!( container_iteration_termination(true, &3, Some(&2)), Some(ContainerLayoutTermination::GeometryStable) ); assert_eq!( container_iteration_termination(false, &3, Some(&3)), Some(ContainerLayoutTermination::SignatureStable) ); assert_eq!(container_iteration_termination(false, &3, Some(&2)), None); assert_eq!(CONTAINER_LAYOUT_SAFETY_LIMIT, 512); } #[test] fn declarative_shadow_style_does_not_leak_into_document_cascade() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let button = tree.get_element_by_id("light-button").unwrap(); let rect = laid.rects[&button]; assert!((rect.width - 123.0).abs() < 0.1, "{rect:?}"); assert!((rect.height - 20.0).abs() < 0.1, "{rect:?}"); } #[test] fn multicol_balances_atomic_blocks_like_chromium() { // Chromium 140 geometry for this reduction is a 630x300 container, // 190px columns at x=0/220/440, and a 2/3/3 source-order partition. // Explicit heights keep this a layout-algorithm regression rather // than a font/raster comparison. let tree = parse_html( "\
\
\
\
\
\
\
\
\
\
", ); let laid = layout_dom(&tree, (800.0, 600.0)); let rect = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()]; let columns = rect("columns"); assert!((columns.width - 630.0).abs() < 0.1, "{columns:?}"); assert!((columns.height - 300.0).abs() < 0.1, "{columns:?}"); let expected = [ ("a", 0.0, 0.0, 190.0, 180.0), ("b", 0.0, 180.0, 190.0, 120.0), ("c", 220.0, 0.0, 190.0, 60.0), ("d", 220.0, 60.0, 190.0, 150.0), ("e", 220.0, 210.0, 190.0, 90.0), ("f", 440.0, 0.0, 190.0, 120.0), ("g", 440.0, 120.0, 190.0, 80.0), ("h", 440.0, 200.0, 190.0, 100.0), ]; for (id, x, y, width, height) in expected { let child = rect(id); assert!((child.x - columns.x - x).abs() < 0.1, "{id}: {child:?}"); assert!((child.y - columns.y - y).abs() < 0.1, "{id}: {child:?}"); assert!((child.width - width).abs() < 0.1, "{id}: {child:?}"); assert!((child.height - height).abs() < 0.1, "{id}: {child:?}"); } let narrow = layout_dom(&tree, (600.0, 1000.0)); let columns_id = tree.get_element_by_id("columns").unwrap(); let c_id = tree.get_element_by_id("c").unwrap(); assert_eq!(narrow.styles[&columns_id].column_count, Some(1)); assert!( (narrow.rects[&columns_id].height - 900.0).abs() < 0.1, "{:?}", narrow.rects[&columns_id] ); assert!( (narrow.rects[&c_id].y - narrow.rects[&columns_id].y - 300.0).abs() < 0.1, "{:?}", narrow.rects[&c_id] ); } #[test] fn static_positions_use_final_post_repair_geometry_without_phantom_overflow() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (800.0, 400.0)); let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()]; let columns = rect("columns"); let holder = rect("holder"); let outer = rect("outer"); let wrapper = rect("wrapper"); let nested = rect("nested"); assert!((columns.height - 200.0).abs() < 0.01, "{columns:?}"); assert!((holder.y - 200.0).abs() < 0.01, "{holder:?}"); assert!( (outer.y - holder.y).abs() < 0.01, "outer static position was harvested before multicol repair: \ holder={holder:?} outer={outer:?}" ); assert!( (nested.y - wrapper.y).abs() < 0.01, "nested static-position candidate changed while reparenting its ancestor: \ wrapper={wrapper:?} nested={nested:?}" ); assert_eq!( laid.scrolling_content_size(&tree, (800.0, 400.0)), (800.0, 400.0), "a stale preliminary static position must not create scrolling overflow" ); } #[test] fn multicol_does_not_atomize_breakable_prose_boxes() { let tree = parse_html( "
\

\

\
", ); let laid = layout_dom(&tree, (400.0, 400.0)); let columns = laid.rects[&tree.get_element_by_id("columns").unwrap()]; let first = laid.rects[&tree.get_element_by_id("first").unwrap()]; let second = laid.rects[&tree.get_element_by_id("second").unwrap()]; assert!((columns.height - 200.0).abs() < 0.1, "{columns:?}"); assert!((first.width - 200.0).abs() < 0.1, "{first:?}"); assert!((second.x - first.x).abs() < 0.1, "{second:?}"); assert!((second.y - first.y - 100.0).abs() < 0.1, "{second:?}"); } #[test] fn direct_flex_text_is_one_wrapping_anonymous_item() { // Chromium 140: the pseudo is one flex item and the direct text run // is one anonymous flex item with a 276px inline formatting context. // The sentence wraps to three 20px lines; treating every word as an // outer flex item instead produces one overflowing 20px line. let tree = parse_html( "\
This anonymous text item must wrap inside the remaining flex space instead of overflowing.
", ); let laid = layout_dom(&tree, (500.0, 200.0)); let quote = laid.rects[&tree.get_element_by_id("quote").unwrap()]; assert!((quote.width - 300.0).abs() < 0.1, "{quote:?}"); assert!((quote.height - 60.0).abs() < 0.1, "{quote:?}"); } #[test] fn auto_width_column_flex_text_uses_fit_content_width() { // Chromium 145: the outer row leaves 533px beside the fixed 203px // sibling and 32px gap. The auto-width quote is fit-content, so its // direct anonymous text item reflows to five 24px lines. Measuring it // only at max-content instead produces a 2097px-wide, 24px-tall line. let tree = parse_html( r#"
MDN closely follows W3C standards which helps me keep up with important topics. It's a complete package as it caters to everything; complex APIs, new browser functionalities, and best practices. MDN serves as a truly valuable resource and continues to assist me in my everyday development.
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] }; let row = rect("row"); let column = rect("column"); let quote = rect("quote"); let logo = rect("logo"); assert!((row.width - 768.0).abs() < 0.1, "{row:?}"); assert!((row.height - 128.0).abs() < 0.1, "{row:?}"); assert!((column.width - 533.0).abs() < 0.1, "{column:?}"); assert!((quote.width - 533.0).abs() < 0.1, "{quote:?}"); assert!((quote.height - 120.0).abs() < 0.1, "{quote:?}"); assert!((logo.x - 565.0).abs() < 0.1, "{logo:?}"); assert!((logo.width - 203.0).abs() < 0.1, "{logo:?}"); } #[test] fn column_flex_fit_content_preserves_unbreakable_min_content() { // Chromium 145 keeps the unbreakable quote at 2561.25px and lets the // constrained row overflow. The synthetic fit-content cap must not // turn an intrinsic min-content floor into overflow-wrap:anywhere. let word = "X".repeat(240); let tree = parse_html(&format!( r#"
{word}
"# )); let laid = layout_dom(&tree, (1280.0, 720.0)); let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] }; let column = rect("column"); let quote = rect("quote"); let logo = rect("logo"); assert!(quote.width > 2500.0, "{quote:?}"); assert!( (column.width - quote.width).abs() < 0.1, "{column:?} {quote:?}" ); assert!((quote.height - 24.0).abs() < 0.1, "{quote:?}"); assert!((logo.x - quote.width - 32.0).abs() < 0.1, "{logo:?}"); } #[test] fn empty_document_is_safe() { // html5ever always synthesizes html/head/body, so an empty document // still has a few element rects. The point is that it does not panic. let tree = parse_html(""); let laid = layout_dom(&tree, (1280.0, 720.0)); assert!(laid.rects.len() <= 4, "got {}", laid.rects.len()); } #[test] fn author_cascade_honors_important_and_presentational_hint_order() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); for id in [ "sheet", "inline-normal", "inline-important", "inline-order", "custom", ] { let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap(); let style = laid.styles.get(&nid).unwrap(); assert_eq!( style.width, crate::Dimension::Px(320.0), "wrong cascade width for {id}" ); } for id in [ "sheet", "inline-normal", "inline-important", "inline-order", "hint", ] { let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap(); let style = laid.styles.get(&nid).unwrap(); assert_eq!( style.background_color, Some([0, 128, 0, 255]), "wrong cascade color for {id}" ); } } #[test] fn box_sizing_controls_the_declared_size_edge() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let size = |id: &str| { let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap(); let rect = laid.rects.get(&nid).unwrap(); (rect.width, rect.height) }; assert_eq!(size("content"), (124.0, 74.0)); assert_eq!(size("border"), (100.0, 50.0)); assert_eq!(size("half-content").0, 224.0); assert_eq!(size("half-border").0, 200.0); assert_eq!(size("max-content").0, 124.0); assert_eq!(size("max-border").0, 100.0); assert_eq!(size("inherited-border").0, 100.0); } #[test] fn logical_sizes_control_final_horizontal_geometry() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (400.0, 240.0)); let size = |id: &str| { let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap(); let rect = laid.rects.get(&nid).unwrap(); (rect.width, rect.height) }; assert_eq!(size("logical-last"), (120.0, 30.0)); assert_eq!(size("physical-last"), (40.0, 15.0)); assert_eq!(size("bounded"), (300.0, 40.0)); } #[test] fn root_percentage_font_size_controls_rem_lengths() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let html = tree.query_selector("html").unwrap().unwrap(); let box_id = tree.query_selector("#box").unwrap().unwrap(); assert_eq!(laid.styles.get(&html).unwrap().font_size, Some(10.0)); assert_eq!(laid.styles.get(&box_id).unwrap().row_gap, Some(40.0)); assert_eq!(laid.styles.get(&box_id).unwrap().column_gap, Some(35.6)); assert_eq!(laid.rects.get(&box_id).unwrap().width, 40.0); assert_eq!(laid.rects.get(&box_id).unwrap().height, 80.0); assert_eq!(laid.rects.get(&box_id).unwrap().y, 30.0); } #[test] fn block_and_grid_sibling_margins_collapse() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let b = tree.query_selector("#b").unwrap().unwrap(); assert_eq!(laid.rects.get(&b).unwrap().y, 50.0); } #[test] fn text_transform_applies_to_word_leaves_in_flex_ui() { let tree = parse_html( r#"
get started
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let cta = tree.get_element_by_id("cta").unwrap(); let items = &laid.run_ifc_items[&cta]; assert_eq!(items.len(), 1); assert_eq!(laid.text_engine.item_text(items[0]), "GET STARTED"); assert_eq!( transform_word_leaf_text("hELLO wORLD", crate::TextTransform::Capitalize), "HELLO WORLD" ); } #[test] fn word_leaves_use_the_computed_line_height() { let tree = parse_html( r#"
code
"#, ); let mut laid = layout_dom(&tree, (1280.0, 720.0)); let code = tree.get_element_by_id("code").unwrap(); let items = &laid.run_ifc_items[&code]; assert_eq!(items.len(), 1); let (_, height) = laid.text_engine.measure(items[0], Some(1280.0)); assert_eq!(height, 32.0); } #[test] fn light_root_rejects_dark_pseudo_gradient_variants() { let tree = parse_html( r#"
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let hero = tree.query_selector(".hero").unwrap().unwrap(); let pseudo = laid.styles[&hero].before_pseudo.as_ref().unwrap(); let (_, stops) = pseudo.background_radial_gradient.as_ref().unwrap(); assert_eq!(stops[0].0, [0xeb, 0xf3, 0xf9, 255]); assert_eq!(stops[1].0, [0xd6, 0xde, 0xe4, 255]); } #[test] fn table_cell_content_box_width_includes_padding_and_border_in_track() { let tree = parse_html( r#"
content
border
"#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let width = |id: &str| { let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap(); laid.rects.get(&nid).unwrap().width }; assert_eq!(width("content-cell"), 124.0); assert_eq!(width("border-cell"), 100.0); } #[test] fn white_space_inherits_into_a_block_descendant_inline_context() { let tree = parse_html( r#"

                alpha beta gamma delta
                alpha beta gamma delta
            
"#, ); let laid = layout_dom(&tree, (400.0, 200.0)); let inherited = tree.get_element_by_id("inherited").unwrap(); let normal = tree.get_element_by_id("normal").unwrap(); assert_eq!( laid.styles[&inherited].white_space, Some(crate::WhiteSpace::Pre), "a block descendant must inherit the preformatted wrapping mode" ); assert_eq!( laid.styles[&normal].white_space, Some(crate::WhiteSpace::Normal), "an explicit descendant value must override inherited pre" ); assert_eq!( laid.styles[&inherited] .before_pseudo .as_ref() .unwrap() .white_space, Some(crate::WhiteSpace::Pre), "a pseudo-element must inherit the originating element's computed wrapping mode" ); assert_eq!( laid.rects[&inherited].height, 20.0, "the inherited pre line must overflow horizontally without wrapping" ); assert!( laid.rects[&normal].height > 20.0, "the explicit normal line should still wrap at the containing width" ); } #[test] fn forced_break_in_pseudo_joined_inline_run_has_no_phantom_line() { let tree = parse_html( r#"

first
second

A

B

"#, ); let laid = layout_dom(&tree, (800.0, 400.0)); let heading = tree.get_element_by_id("heading").unwrap(); let heading_break = tree.get_element_by_id("heading-break").unwrap(); let consecutive = tree.get_element_by_id("consecutive").unwrap(); let break_style = &laid.styles[&heading_break]; assert_eq!(break_style.display, crate::Display::Inline); assert_eq!(break_style.width, crate::Dimension::Auto); assert_eq!(break_style.height, crate::Dimension::Auto); assert!( (laid.rects[&heading].height - 120.0).abs() < 0.01, "A
B plus an inline pseudo must form two 60px lines: host={:?}, br={:?}, pseudo={}, run_ifc={}", laid.rects[&heading], laid.rects.get(&heading_break), laid.styles[&heading].after_pseudo.is_some(), laid.run_ifc_items .get(&heading) .map_or(0, |items| items.len()) ); assert_eq!( laid.rects .get(&heading_break) .map_or(0.0, |rect| rect.width), 0.0, "the mapped BR marker must not expose the anonymous breaker width" ); assert!( (laid.rects[&consecutive].height - 60.0).abs() < 0.01, "A

B must form three 20px lines: {:?}", laid.rects[&consecutive] ); } #[test] fn ua_block_margins_resolve_against_the_computed_element_font() { let tree = parse_html( r#"

Heading

Paragraph

H1

H2

H3

H4

H5
H6

Inherited

Initial

      "#, ); let laid = layout_dom(&tree, (800.0, 400.0)); let heading = tree.get_element_by_id("heading").unwrap(); let paragraph = tree.get_element_by_id("paragraph").unwrap(); assert!( (laid.styles[&heading].margin.top - 26.8).abs() < 0.01 && (laid.styles[&heading].margin.bottom - 26.8).abs() < 0.01, "h1's 0.67em UA margins must follow its authored 40px font: {:?}", laid.styles[&heading].margin ); assert!( (laid.styles[¶graph].margin.top - 24.0).abs() < 0.01 && (laid.styles[¶graph].margin.bottom - 24.0).abs() < 0.01, "p's 1em UA margins must follow its authored 24px font: {:?}", laid.styles[¶graph].margin ); for (id, size, margin) in [ ("h1", 40.0, 26.8), ("h2", 30.0, 24.9), ("h3", 23.4, 23.4), ("h4", 20.0, 26.6), ("h5", 16.6, 27.722), ("h6", 13.4, 31.222), ] { let style = &laid.styles[&tree.get_element_by_id(id).unwrap()]; assert!( (style.font_size.unwrap() - size).abs() < 0.01 && (style.margin.top - margin).abs() < 0.01 && (style.margin.bottom - margin).abs() < 0.01, "{id} UA font and margins must resolve from the inherited 20px font: size={:?}, margin={:?}", style.font_size, style.margin ); } for (id, size, margin) in [("inherit-size", 20.0, 13.4), ("initial-size", 16.0, 10.72)] { let style = &laid.styles[&tree.get_element_by_id(id).unwrap()]; assert!( (style.font_size.unwrap() - size).abs() < 0.01 && (style.margin.top - margin).abs() < 0.01, "{id} CSS-wide font-size semantics must override the UA heading size: size={:?}, margin={:?}", style.font_size, style.margin ); } for (id, marker) in [ ("second-list", crate::ListStyle::Circle), ("third-list", crate::ListStyle::Square), ("definition-list-child", crate::ListStyle::Disc), ] { let style = &laid.styles[&tree.get_element_by_id(id).unwrap()]; assert_eq!(style.margin.top, 0.0, "{id} must lose nested UA margins"); assert_eq!(style.margin.bottom, 0.0, "{id} must lose nested UA margins"); assert_eq!(style.list_style, Some(marker), "{id} marker depth"); } } #[test] fn generated_inline_boxes_apply_sizes_only_after_display_blockification() { let tree = parse_html( r#"
      "#, ); let laid = layout_dom(&tree, (500.0, 300.0)); let flex = tree.get_element_by_id("flex").unwrap(); let float = tree.get_element_by_id("float").unwrap(); let relative = tree.get_element_by_id("relative").unwrap(); let pseudo = |host| { laid.generated_boxes .iter() .find(|generated| { generated.host == host && generated.kind == GeneratedBoxKind::Before }) .unwrap() .rect }; assert_eq!( laid.styles[&flex].before_pseudo.as_ref().unwrap().display, crate::Display::Block, "a generated flex item must be blockified" ); assert_eq!( laid.styles[&float].before_pseudo.as_ref().unwrap().display, crate::Display::Block, "a floated generated box must be blockified" ); assert_eq!( laid.styles[&relative] .before_pseudo .as_ref() .unwrap() .display, crate::Display::Inline, "relative positioning does not make an ordinary inline atomic" ); assert_eq!(pseudo(flex).width, 250.0); assert_eq!(pseudo(flex).height, 120.0); assert_eq!(pseudo(float).width, 80.0); assert_eq!(pseudo(float).height, 40.0); assert!( pseudo(relative).width < 80.0 && pseudo(relative).height < 40.0, "an ordinary inline pseudo must ignore authored box sizes" ); } #[test] fn ordinary_inline_fragment_uses_font_box_while_line_keeps_line_height() { let tree = parse_html( r#"

      token

      token

      "#, ); let laid = layout_dom(&tree, (400.0, 200.0)); let host = tree.get_element_by_id("with-margin").unwrap(); let control_host = tree.get_element_by_id("without-margin").unwrap(); let token = tree.get_element_by_id("token").unwrap(); let control = tree.get_element_by_id("control").unwrap(); let style = &laid.styles[&token]; let raw_font_height = laid.text_engine.inline_font_box_height(style); let expected_fragment_height = raw_font_height + 5.0 + 7.0 + 2.0 + 3.0; assert_eq!( laid.rects[&host].height, 40.0, "block-axis inline padding/border must not increase line advance" ); assert_eq!(laid.rects[&control_host].height, 40.0); assert_eq!(laid.rects[&token].height, expected_fragment_height); assert_eq!(laid.inline_fragments[&token], vec![laid.rects[&token]]); assert!( ((laid.rects[&token].y - laid.rects[&host].y) - ((40.0 - raw_font_height) / 2.0 - 5.0 - 2.0)) .abs() < 0.5, "final shaped baseline must retain the expected centered font box" ); assert_eq!( laid.rects[&control].y - laid.rects[&control_host].y, laid.rects[&token].y - laid.rects[&host].y, "ordinary inline block-axis margins must not move its fragment" ); } #[test] fn nested_inline_uses_final_shaping_for_its_canonical_fragment() { let tree = parse_html( r#"

      before plain nested text after

      "#, ); let laid = layout_dom(&tree, (400.0, 100.0)); let token = tree.get_element_by_id("token").unwrap(); let fragments = &laid.inline_fragments[&token]; assert_eq!(fragments.len(), 1, "{fragments:?}"); assert_eq!(laid.rects[&token], fragments[0]); assert!(fragments[0].width > 100.0, "{fragments:?}"); assert_eq!( laid.ifc_items.len(), 1, "the span must stay in one shaped IFC" ); } #[test] fn wrapped_decorated_inline_exposes_three_ordered_font_box_fragments() { let tree = parse_html( r#"

      alpha beta gamma

      "#, ); let laid = layout_dom(&tree, (300.0, 120.0)); let token = tree.get_element_by_id("token").unwrap(); let fragments = &laid.inline_fragments[&token]; assert_eq!( fragments.len(), 3, "one canonical fragment per visual line: {fragments:?}" ); assert!(fragments.windows(2).all(|pair| pair[0].y < pair[1].y)); assert!(fragments.iter().all(|fragment| fragment.width > 0.0)); let left = fragments .iter() .map(|rect| rect.x) .fold(f32::INFINITY, f32::min); let top = fragments .iter() .map(|rect| rect.y) .fold(f32::INFINITY, f32::min); let right = fragments .iter() .map(|rect| rect.x + rect.width) .fold(f32::NEG_INFINITY, f32::max); let bottom = fragments .iter() .map(|rect| rect.y + rect.height) .fold(f32::NEG_INFINITY, f32::max); assert_eq!( laid.rects[&token], Rect { x: left, y: top, width: right - left, height: bottom - top } ); } #[test] fn inline_horizontal_edges_advance_adjacent_content_but_margins_stay_outside_rect() { let tree = parse_html( r#"

      aabb

      aabb

      "#, ); let laid = layout_dom(&tree, (400.0, 100.0)); let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] }; let local_x = |id: &str, host: &str| rect(id).x - rect(host).x; assert!((rect("token").width - rect("plain").width - 12.0).abs() < 0.01); assert!( (local_x("token", "decorated") - local_x("plain", "plain-host") - 3.0).abs() < 0.01 ); assert!( (local_x("after", "decorated") - local_x("plain-after", "plain-host") - 20.0).abs() < 0.01 ); assert!((rect("after").x - (rect("token").x + rect("token").width) - 5.0).abs() < 0.01); } #[test] fn inline_edges_participate_in_wrapping_and_slice_only_outer_continuation_sides() { let tree = parse_html( r#"

      aaaa aaaa aaaa

      "#, ); let laid = layout_dom(&tree, (200.0, 100.0)); let token = tree.get_element_by_id("token").unwrap(); let fragments = &laid.inline_fragments[&token]; assert_eq!(fragments.len(), 3, "{fragments:?}"); assert!( (fragments[0].width - fragments[1].width - 12.0).abs() < 0.01, "{fragments:?}" ); assert!( fragments[2].width > fragments[1].width, "last continuation owns its end side: {fragments:?}" ); assert!(fragments.iter().all(|fragment| fragment.width <= 70.01)); } #[test] fn empty_decorated_inline_has_a_box_and_advances_following_text() { let tree = parse_html( r#"

      xy

      xy

      "#, ); let laid = layout_dom(&tree, (300.0, 100.0)); let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] }; assert!( (rect("empty").width - 14.0).abs() < 0.01, "{:?}", rect("empty") ); assert!( (rect("empty").height - laid.text_engine.inline_font_box_height( &laid.styles[&tree.get_element_by_id("empty").unwrap()] ) - 6.0) .abs() < 0.01 ); let decorated_after = rect("after").x - rect("decorated").x; let plain_after = rect("plain-after").x - rect("plain-host").x; assert!((decorated_after - plain_after - 20.0).abs() < 0.01); } #[test] fn inline_provenance_maps_repeated_multibyte_text_across_hard_breaks() { let tree = parse_html( r#"

      é界
      é界

      "#, ); let laid = layout_dom(&tree, (300.0, 100.0)); let token = tree.get_element_by_id("token").unwrap(); let fragments = &laid.inline_fragments[&token]; assert_eq!(fragments.len(), 2, "{fragments:?}"); assert!( (fragments[0].width - fragments[1].width).abs() < 0.01, "{fragments:?}" ); assert!( (fragments[1].y - fragments[0].y - 24.0).abs() < 0.01, "{fragments:?}" ); } #[test] fn relative_inline_offsets_move_canonical_fragments_in_pixels_and_percentages() { let tree = parse_html( r#"

      token

      token

      token

      "#, ); let laid = layout_dom(&tree, (400.0, 120.0)); let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] }; let local = |token: &str, host: &str| { let token = rect(token); let host = rect(host); (token.x - host.x, token.y - host.y) }; let base = local("base", "base-host"); let pixels = local("pixels", "pixel-host"); let percent = local("percent", "percent-host"); assert!( (pixels.0 - base.0 - 7.0).abs() < 0.01, "{base:?} {pixels:?}" ); assert!( (pixels.1 - base.1 - 5.0).abs() < 0.01, "{base:?} {pixels:?}" ); assert!( (percent.0 - base.0 - 20.0).abs() < 0.01, "{base:?} {percent:?}" ); assert!( (percent.1 - base.1 - 2.0).abs() < 0.01, "{base:?} {percent:?}" ); } #[test] fn canonical_inline_fragments_shape_with_the_loaded_webfont() { let tree = parse_html( r#"

      WWWWiiii

      "#, ); let token = tree.get_element_by_id("token").unwrap(); let fallback = layout_dom(&tree, (500.0, 150.0)); let loaded = layout_dom_with_web_fonts( &tree, (500.0, 150.0), &HashMap::new(), &[crate::inline::WebFont { data: include_bytes!("../assets/liberation-serif.ttf").to_vec(), family: Some("Fixture".to_string()), weight: Some((400, 400)), italic: Some(false), }], ); assert!(loaded.inline_fragments.contains_key(&token)); assert_ne!( fallback.rects[&token].width, loaded.rects[&token].width, "the loaded face's advances must drive fallback inline geometry" ); assert_ne!( fallback.rects[&token].height, loaded.rects[&token].height, "the loaded face's raw ascent/descent must drive the visual fragment" ); assert_eq!( loaded.inline_fragments[&token], vec![loaded.rects[&token]], "the loaded-font fragment must be the geometry/paint source" ); } #[test] fn positioned_pseudo_inherits_the_hosts_computed_font_metrics() { let tree = parse_html( r#" "#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let cta = tree.get_element_by_id("cta").unwrap(); let host = &laid.styles[&cta]; let pseudo = host.after_pseudo.as_ref().unwrap(); assert_eq!(host.font_size, Some(14.0)); assert_eq!(pseudo.font_size, host.font_size); assert_eq!(pseudo.font_family, host.font_family); assert_eq!(pseudo.font_weight, host.font_weight); assert_eq!(pseudo.line_height, host.line_height); } #[test] fn positioned_pseudo_does_not_wrap_shrink_to_fit_host_text() { let tree = parse_html( r#" "#, ); let laid = layout_dom(&tree, (500.0, 300.0)); let host = tree.get_element_by_id("host").unwrap(); let rect = laid.rects[&host]; assert!( (120.0..=130.0).contains(&rect.width), "max-content width must include the shaped inter-word advance: {rect:?}" ); assert_eq!( rect.height, 40.0, "positioned generated content must not make the host label wrap" ); } #[test] fn variable_font_properties_inherit_and_reset_through_elements_and_pseudos() { let tree = parse_html( r#"
      "#, ); let laid = layout_dom(&tree, (1280.0, 720.0)); let get = |selector: &str| { let id = tree.query_selector(selector).unwrap().unwrap(); &laid.styles[&id] }; let parent = get("#parent"); let inherited = get("#inherited"); assert_eq!( parent.font_optical_sizing, Some(crate::FontOpticalSizing::None) ); assert_eq!(inherited.font_optical_sizing, parent.font_optical_sizing); assert_eq!( inherited.font_variation_settings, parent.font_variation_settings ); let before = parent.before_pseudo.as_ref().unwrap(); assert_eq!(before.font_optical_sizing, parent.font_optical_sizing); assert_eq!( before.font_variation_settings, parent.font_variation_settings ); let reset = get("#reset"); let reset_child = get("#reset-child"); assert_eq!( reset.font_optical_sizing, Some(crate::FontOpticalSizing::Auto) ); assert_eq!(reset.font_variation_settings, Some(Vec::new())); assert_eq!(reset_child.font_optical_sizing, reset.font_optical_sizing); assert_eq!( reset_child.font_variation_settings, reset.font_variation_settings ); let after = reset.after_pseudo.as_ref().unwrap(); assert_eq!(after.font_optical_sizing, reset.font_optical_sizing); assert_eq!( after.font_variation_settings.as_deref(), Some( [crate::FontVariationSetting { tag: *b"wght", value: 300.0, }] .as_slice() ) ); } #[test] fn native_select_sizes_for_its_widest_option() { let tree = parse_html( r#""#, ); let laid = layout_dom(&tree, (500.0, 200.0)); let select = tree.get_element_by_id("language").unwrap(); let rect = laid.rects[&select]; assert!( rect.width > 120.0, "widest option should set width: {rect:?}" ); assert!( (18.0..=22.0).contains(&rect.height), "closed native select should have one-line control height: {rect:?}" ); } #[test] fn grid_auto_columns_size_successive_implicit_column_tracks() { let tree = parse_html( r#"
      "#, ); let laid = layout_dom(&tree, (500.0, 200.0)); let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()]; assert_eq!( rect("first"), Rect { x: 0.0, y: 0.0, width: 100.0, height: 20.0 } ); assert_eq!( rect("second"), Rect { x: 100.0, y: 0.0, width: 50.0, height: 20.0 } ); assert_eq!( rect("third"), Rect { x: 150.0, y: 0.0, width: 50.0, height: 20.0 } ); } #[test] fn grid_auto_rows_size_successive_implicit_row_tracks() { let tree = parse_html( r#"
      "#, ); let laid = layout_dom(&tree, (500.0, 400.0)); let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()]; assert_eq!( rect("first"), Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 } ); assert_eq!( rect("second"), Rect { x: 0.0, y: 100.0, width: 100.0, height: 50.0 } ); assert_eq!( rect("third"), Rect { x: 0.0, y: 150.0, width: 100.0, height: 50.0 } ); } #[test] fn grid_auto_track_inherit_uses_the_parent_computed_lists() { let tree = parse_html( r#"
      "#, ); let laid = layout_dom(&tree, (500.0, 300.0)); let grid = tree.get_element_by_id("grid").unwrap(); let implicit = tree.get_element_by_id("implicit").unwrap(); assert_eq!(laid.rects[&implicit].x, 100.0); assert_eq!(laid.rects[&implicit].width, 60.0); assert_eq!(laid.styles[&grid].grid_auto_columns.len(), 1); assert_eq!(laid.styles[&grid].grid_auto_rows.len(), 1); assert!(!laid.styles[&grid].grid_auto_columns_inherit); assert!(!laid.styles[&grid].grid_auto_rows_inherit); } #[cfg(feature = "paint")] #[test] fn text_break_modes_match_chromium_fixed_width_geometry() { // Chromium 145 at DPR 1 with Liberation Sans produces the same // 20/40px line boxes. `break-word` and `anywhere` are emergency // wrapping modes; `break-all` adds typographic-letter opportunities. // keep-all must retain ordinary Latin word/space behavior. let tree = parse_html( r#"
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      alpha beta gamma
      alpha beta gamma
      abcdefghijklmnop
      "#, ); let laid = layout_dom(&tree, (500.0, 500.0)); let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()]; assert_eq!( rect("normal"), Rect { x: 0.0, y: 0.0, width: 80.0, height: 20.0 } ); assert_eq!( rect("break-word"), Rect { x: 0.0, y: 20.0, width: 80.0, height: 40.0 } ); assert_eq!( rect("anywhere"), Rect { x: 0.0, y: 60.0, width: 80.0, height: 40.0 } ); assert_eq!( rect("break-all"), Rect { x: 0.0, y: 100.0, width: 80.0, height: 40.0 } ); assert_eq!(rect("latin-normal").height, 40.0); assert_eq!(rect("keep-all").height, rect("latin-normal").height); assert_eq!(rect("legacy").height, 40.0); } #[cfg(feature = "paint")] #[test] fn text_break_inheritance_alias_and_css_wide_values_match_chromium() { let tree = parse_html( r#"
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      "#, ); let laid = layout_dom(&tree, (500.0, 500.0)); let node = |id| tree.get_element_by_id(id).unwrap(); let style = |id| &laid.styles[&node(id)]; let height = |id| laid.rects[&node(id)].height; assert_eq!( style("inherited").overflow_wrap, Some(crate::OverflowWrap::Anywhere) ); assert_eq!( style("inherited").word_break, Some(crate::WordBreak::BreakAll) ); assert_eq!(height("inherited"), 40.0); assert_eq!( style("inherited") .before_pseudo .as_ref() .unwrap() .overflow_wrap, Some(crate::OverflowWrap::Anywhere) ); assert_eq!( style("inherited") .before_pseudo .as_ref() .unwrap() .word_break, Some(crate::WordBreak::BreakAll) ); assert_eq!( style("initial").overflow_wrap, Some(crate::OverflowWrap::Normal) ); assert_eq!(style("initial").word_break, Some(crate::WordBreak::Normal)); assert_eq!(height("initial"), 20.0); for id in ["unset", "inherit", "revert", "revert-layer"] { assert_eq!( style(id).overflow_wrap, Some(crate::OverflowWrap::Anywhere), "{id}" ); assert_eq!( style(id).word_break, Some(crate::WordBreak::BreakAll), "{id}" ); assert_eq!(height(id), 40.0, "{id}"); } assert_eq!( style("alias").overflow_wrap, Some(crate::OverflowWrap::Anywhere) ); assert_eq!(style("alias").word_break, Some(crate::WordBreak::Normal)); assert_eq!(height("alias"), 40.0); } #[cfg(feature = "paint")] #[test] fn anywhere_but_not_break_word_reduces_min_content() { let tree = parse_html( r#"
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      "#, ); let laid = layout_dom(&tree, (500.0, 1000.0)); let width = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()].width; assert_eq!(width("normal"), 125.0); assert_eq!(width("break-word"), 125.0); assert_eq!(width("anywhere"), 14.0); assert_eq!(width("break-all"), 14.0); } #[cfg(feature = "paint")] #[test] fn inline_descendant_break_policies_stay_at_their_style_boundaries() { let tree = parse_html( r#"
      abcdefghijklmnop
      abcdefghijklmnop
      abcdefghijklmnop
      "#, ); let laid = layout_dom(&tree, (500.0, 300.0)); let height = |id| laid.rects[&tree.get_element_by_id(id).unwrap()].height; assert_eq!(height("child-break-all"), 40.0); assert_eq!(height("child-normal"), 20.0); assert_eq!(height("child-anywhere"), 40.0); } #[cfg(feature = "paint")] #[test] fn break_all_keeps_uax14_punctuation_pairs_in_min_content() { let tree = parse_html( r#"
      (ab)
      (ab)
      "#, ); let laid = layout_dom(&tree, (500.0, 300.0)); let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()]; assert!(rect("break-all").width > rect("anywhere").width); assert_eq!(rect("break-all").height, 40.0); assert_eq!(rect("anywhere").height, 80.0); } #[cfg(feature = "paint")] #[test] fn break_all_uses_blink_punctuation_pair_semantics() { let tree = parse_html( r#"
      )a
      )a
      a)
      a)
      a/b
      a/b
      a-b
      a-b
      a|b
      a|b
      a+b
      a+b
      $a
      $a
      a%
      a%
      "#, ); let laid = layout_dom(&tree, (500.0, 1000.0)); let width = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()].width; for stem in ["close", "slash", "hyphen", "bar", "plus"] { assert!( width(&format!("{stem}-break")) < width(&format!("{stem}-normal")), "break-all should expose a narrower opportunity for {stem}" ); } for stem in ["closing", "prefix", "postfix"] { assert_eq!( width(&format!("{stem}-break")), width(&format!("{stem}-normal")), "break-all must retain the prohibited boundary for {stem}" ); } } #[cfg(feature = "paint")] #[test] fn keep_all_suppresses_mixed_cjk_latin_word_boundaries() { let tree = parse_html( r#"
      標(準)萬ab國.碼
      標(準)萬ab國.碼
      "#, ); let laid = layout_dom(&tree, (500.0, 500.0)); let width = |id| laid.rects[&tree.get_element_by_id(id).unwrap()].width; assert!(width("keep") > width("normal")); } }