//! 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 obscura_dom::tree::{DomTree, NodeId}; use taffy::prelude::*; use crate::{Rect, to_taffy_style}; /// 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), ); 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(), } ); } #[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| 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| 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 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_eq!( laid.rects[&token].y - laid.rects[&host].y, (40.0 - raw_font_height) / 2.0 - 5.0 - 2.0 ); 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 fallback_word_boxes_shape_and_paint_with_the_loaded_webfont() { let tree = parse_html( r#"

    WWWWiiii

    "#, ); let token = tree.get_element_by_id("token").unwrap(); let text = rendered_children(&tree, token)[0]; 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.word_ifc_items.get(&text).is_some_and(|items| !items.is_empty()), "a decorated/relative inline fallback must retain shaped word items" ); 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")); } }