//! Regression test: a Hacker-News-shaped nested `
` layout must come out
//! right using only the general engine (UA defaults + real CSS cascade), with
//! no per-site hardcoded selectors. This guards against reintroducing the
//! site-specific hacks that used to live in obscura-render for this exact markup.
use obscura_dom::tree_sink::parse_html;
use obscura_render::{layout_dom, layout_dom_with_images};
use std::collections::HashMap;
const HN_HTML: &str = r##"
"#,
);
let layout = layout_dom(&tree, (900.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let row = rect("row");
let items = [rect("one"), rect("two"), rect("three"), rect("four")];
assert!(
(row.width - 900.0).abs() < 0.01,
"text-align must not shrink-wrap an auto-width block child: {row:?}"
);
assert!(
items.windows(2).all(|pair| (pair[0].y - pair[1].y).abs() < 0.01),
"inline-block items should share one line when the block has room: {items:?}"
);
assert!(
items[0].height <= 40.01,
"auto-width inline-block text should use one max-content line: {:?}",
items[0]
);
}
#[test]
fn inline_svg_derives_auto_height_from_view_box() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (900.0, 1000.0));
let logo = layout.rects[&tree.get_element_by_id("logo").unwrap()];
assert!((logo.width - 112.0).abs() < 0.01, "svg width: {logo:?}");
assert!(
(logo.height - 101.0).abs() < 0.01,
"viewBox should supply the intrinsic auto-height (device-pixel rounded): {logo:?}"
);
}
/// Chromium 150 resolves a ratio-only inline SVG as an atomic replaced item
/// during both intrinsic grid track sizing and the final percentage-width
/// pass. The 270px track leaves 254px after the grid item's inline padding;
/// the 320:96 viewBox then contributes 76.1875px of height.
#[test]
fn ratio_only_inline_svg_sizes_inside_an_auto_grid_row() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (800.0, 600.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let cell = rect("cell");
let logo = rect("logo");
assert!((logo.width - 254.0).abs() < 0.01, "SVG width: {logo:?}");
assert!(
(logo.height - 76.1875).abs() < 0.01,
"viewBox ratio must transfer the final grid width: {logo:?}"
);
assert!(
(cell.height - 108.1875).abs() < 0.01,
"SVG contribution plus block padding must size the auto row: {cell:?}"
);
}
#[test]
fn inline_block_flex_items_keep_block_inner_flow() {
let tree = parse_html(include_str!("../../../render-repros/inline-block-flex-items.html"));
let layout = layout_dom(&tree, (900.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let brand = rect("brand");
let title = rect("title");
let links = rect("links");
let one = rect("one");
let two = rect("two");
let three = rect("three");
let chips = rect("chips");
let alpha = rect("alpha");
let beta = rect("beta");
let gamma = rect("gamma");
assert!(
(brand.x - 0.0).abs() < 0.01
&& (brand.width - 382.0).abs() < 0.01
&& (title.x - 32.0).abs() < 0.01
&& (title.width - 350.0).abs() < 0.01,
"brand: {brand:?}, title: {title:?}"
);
assert!(
(links.x - 525.0).abs() < 0.01
&& (links.width - 90.0).abs() < 0.01
&& (one.y - 0.0).abs() < 0.01
&& (two.y - 20.0).abs() < 0.01
&& (three.y - 40.0).abs() < 0.01,
"links: {links:?}, items: {one:?} {two:?} {three:?}"
);
assert!(
(chips.x - 0.0).abs() < 0.01
&& (chips.y - 140.0).abs() < 0.01
&& (chips.width - 90.0).abs() < 0.01
&& (chips.height - 20.0).abs() < 0.01
&& (alpha.x - 0.0).abs() < 0.01
&& (beta.x - 30.0).abs() < 0.01
&& (gamma.x - 60.0).abs() < 0.01,
"chips: {chips:?}, items: {alpha:?} {beta:?} {gamma:?}"
);
}
#[test]
fn percentage_height_under_indefinite_parent_uses_content_height() {
let tree = parse_html(include_str!(
"../../../render-repros/percentage-height-indefinite-parent.html"
));
let layout = layout_dom(&tree, (900.0, 1000.0));
for id in ["wrapper", "stack", "editor", "pre", "code"] {
let rect = layout.rects[&tree.get_element_by_id(id).unwrap()];
assert!(
rect.height >= 24.0,
"{id} collapsed despite its indefinite percentage height: {rect:?}"
);
}
}
#[test]
fn item_self_alignment_places_flex_and_grid_items() {
let tree = parse_html(include_str!("../../../render-repros/item-self-alignment.html"));
let layout = layout_dom(&tree, (900.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let flex_center = rect("flex-center");
let flex_end = rect("flex-end");
let grid_end = rect("grid-end");
let grid_center = rect("grid-center");
let grid_place = rect("grid-place");
let parent_one = rect("parent-one");
let parent_two = rect("parent-two");
let line_one = rect("line-one");
let line_two = rect("line-two");
let track_one = rect("track-one");
let track_two = rect("track-two");
assert!(
(flex_center.x - 0.0).abs() < 0.01
&& (flex_center.y - 40.0).abs() < 0.01
&& (flex_end.x - 40.0).abs() < 0.01
&& (flex_end.y - 70.0).abs() < 0.01,
"flex items: {flex_center:?} {flex_end:?}"
);
assert!(
(grid_end.x - 110.0).abs() < 0.01
&& (grid_end.y - 165.0).abs() < 0.01
&& (grid_center.x - 200.0).abs() < 0.01
&& (grid_center.y - 220.0).abs() < 0.01
&& (grid_place.x - 420.0).abs() < 0.01
&& (grid_place.y - 160.0).abs() < 0.01,
"grid items: {grid_end:?} {grid_center:?} {grid_place:?}"
);
assert!(
(parent_one.x - 55.0).abs() < 0.01
&& (parent_one.y - 340.0).abs() < 0.01
&& (parent_two.x - 195.0).abs() < 0.01
&& (parent_two.y - 330.0).abs() < 0.01,
"parent-aligned grid items: {parent_one:?} {parent_two:?}"
);
assert!(
(line_one.x - 0.0).abs() < 0.01
&& (line_one.y - 380.0).abs() < 0.01
&& (line_two.x - 0.0).abs() < 0.01
&& (line_two.y - 480.0).abs() < 0.01,
"aligned flex lines: {line_one:?} {line_two:?}"
);
assert!(
(track_one.x - 100.0).abs() < 0.01
&& (track_one.y - 570.0).abs() < 0.01
&& (track_two.x - 100.0).abs() < 0.01
&& (track_two.y - 620.0).abs() < 0.01,
"aligned grid tracks: {track_one:?} {track_two:?}"
);
}
#[test]
fn non_stretched_auto_grid_item_shrink_wraps_inside_grid_area() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (1280.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let wide_panel = rect("wide-panel");
let wide_wrapper = rect("wide-wrapper");
let wide_inner = rect("wide-inner");
let narrow_wrapper = rect("narrow-wrapper");
assert!(
(wide_wrapper.x - 160.0).abs() < 0.01
&& (wide_wrapper.width - 1072.0).abs() < 0.01
&& (wide_inner.x - 160.0).abs() < 0.01
&& (wide_inner.width - 1072.0).abs() < 0.01,
"wide shrink-wrapped item should be clamped to the grid area: \
panel={wide_panel:?} wrapper={wide_wrapper:?} inner={wide_inner:?}"
);
assert!(
(narrow_wrapper.x - 100.0).abs() < 0.01
&& (narrow_wrapper.width - 200.0).abs() < 0.01,
"a narrower item should keep its intrinsic width and remain centered: {narrow_wrapper:?}"
);
}
#[test]
fn percentage_max_width_clamps_grid_item_intrinsic_size() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (1280.0, 200.0));
let grid = layout.rects[&tree.get_element_by_id("grid").unwrap()];
let item = layout.rects[&tree.get_element_by_id("item").unwrap()];
let gallery = layout.rects[&tree.get_element_by_id("gallery").unwrap()];
assert!(
(grid.width - 1200.0).abs() < 0.01
&& (item.width - 1200.0).abs() < 0.01
&& (gallery.width - 1200.0).abs() < 0.01,
"max-width:100% must clamp the grid item while its flex contents overflow: \
grid={grid:?} item={item:?} gallery={gallery:?}"
);
}
#[test]
fn named_area_shorthand_survives_end_longhand_override() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (1280.0, 200.0));
let hero = layout.rects[&tree.get_element_by_id("hero").unwrap()];
assert!(
(hero.x - 216.0).abs() < 0.01 && (hero.width - 984.0).abs() < 0.01,
"the end longhand must retain the shorthand's named content start: {hero:?}"
);
}
#[test]
fn auto_width_flex_button_keeps_native_intrinsic_sizing() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (800.0, 200.0));
let button = layout.rects[&tree.get_element_by_id("search").unwrap()];
assert!(
button.width > 120.0
&& button.width < 220.0
&& (button.x - (600.0 - button.width) / 2.0).abs() < 0.01,
"the auto-width flex button should shrink-wrap and center: {button:?}"
);
}
#[test]
fn flex_auto_margin_absorbs_space_before_justify_content() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (1280.0, 200.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let crumb = rect("crumb");
let theme = rect("theme");
let language = rect("language");
assert!(
(crumb.x - 0.0).abs() < 0.01
&& (theme.x - 1042.0).abs() < 0.01
&& (language.x - 1149.0).abs() < 0.01,
"main-axis auto margin must consume free space before justify-content: \
crumb={crumb:?} theme={theme:?} language={language:?}"
);
}
#[test]
fn nested_is_descendant_utility_blockifies_matching_lines() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (900.0, 1000.0));
let hero = layout.rects[&tree.get_element_by_id("hero").unwrap()];
assert!(
(hero.width - 800.0).abs() < 0.01,
"span-4 child should fill all four tracks: {hero:?}"
);
}
#[test]
fn length_line_height_computes_before_inheritance() {
let tree = parse_html(
r#"
Inherited length
Unitless ratio
"#,
);
let layout = layout_dom(&tree, (900.0, 1000.0));
let parent = tree.get_element_by_id("parent").unwrap();
let child = tree.get_element_by_id("child").unwrap();
let ratio = tree.get_element_by_id("ratio").unwrap();
assert_eq!(
layout.styles[&parent].line_height,
Some(obscura_render::LineHeight::Px(80.0))
);
assert_eq!(
layout.styles[&child].line_height,
Some(obscura_render::LineHeight::Px(80.0))
);
assert_eq!(
layout.styles[&ratio].line_height,
Some(obscura_render::LineHeight::Ratio(2.0))
);
}
#[test]
fn font_weight_computes_numerically_through_inheritance() {
let tree = parse_html(
r#"
mediumnormalbolderlighter
"#,
);
let layout = layout_dom(&tree, (900.0, 1000.0));
let weight = |id| {
layout.styles[&tree.get_element_by_id(id).unwrap()]
.font_weight
.as_deref()
};
assert_eq!(weight("medium"), Some("500"));
assert_eq!(weight("inherited"), Some("500"));
assert_eq!(weight("normal"), Some("400"));
assert_eq!(weight("bolder"), Some("700"));
assert_eq!(weight("lighter"), Some("100"));
}
#[test]
fn contextual_right_inset_uses_root_font_tokens() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (900.0, 1000.0));
let search = tree.get_element_by_id("search").unwrap();
assert_eq!(
layout.styles[&search].inset[1],
Some(obscura_render::Dimension::Px(56.0))
);
let rect = layout.rects[&search];
assert!(
(rect.x - 744.0).abs() < 0.01,
"right inset should place 100px box at 900 - 56 - 100: {rect:?}"
);
}
#[test]
fn false_legacy_supports_probe_does_not_override_modern_line_height() {
// Tailwind v4 uses this old-browser probe around a leading-variable reset.
// Chromium rejects the condition. Flattening every @supports body made
// the reset win, invalidated the var(), and inflated three 96px lines from
// 288px to an inherited 432px.
let tree = parse_html(
r#"
one two three
"#,
);
let layout = layout_dom(&tree, (1280.0, 720.0));
let hero = layout.rects[&tree.get_element_by_id("hero").unwrap()];
assert!(
(hero.height - 288.0).abs() < 0.01,
"three 96px modern lines should be 288px tall: {hero:?}"
);
}
#[test]
fn custom_property_css_wide_keywords_preserve_var_fallback_semantics() {
let tree = parse_html(
r#"
fallback
inherit
nested fallback
"#,
);
let layout = layout_dom(&tree, (800.0, 600.0));
let invalid = tree.get_element_by_id("invalid").unwrap();
let inherited = tree.get_element_by_id("inherited").unwrap();
let nested = tree.get_element_by_id("nested").unwrap();
assert_eq!(
layout.styles[&invalid].color,
Some([0x44, 0x55, 0x66, 0xff]),
"`initial` is guaranteed-invalid and var() must use its fallback"
);
assert_eq!(
layout.styles[&inherited].color,
Some([0x11, 0x22, 0x33, 0xff]),
"`unset` inherits a custom property's parent value"
);
assert_eq!(
layout.styles[&nested].background_color,
Some([0xff, 0xff, 0xff, 0xff]),
"an invalid nested custom property must activate the outer fallback"
);
}
#[test]
fn stable_root_scrollbar_gutter_reduces_the_initial_containing_block() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (900.0, 600.0));
let fill = layout.rects[&tree.get_element_by_id("fill").unwrap()];
let center = layout.rects[&tree.get_element_by_id("center").unwrap()];
assert!(
(fill.width - 885.0).abs() < 0.01,
"stable classic gutter should reserve 15px: {fill:?}"
);
assert!(
(center.x - 242.0).abs() < 0.01,
"401px box should center in the 885px ICB: {center:?}"
);
}
#[test]
fn auto_grid_track_freezes_at_growth_limit_during_distribution() {
// The first auto track reaches its max-content growth limit before the
// second. The remaining free space belongs only to the second track.
// Giving the frozen track that remainder again makes the grid overflow
// even though the two automatic minimums fit the definite container.
let tree = parse_html(
r#"
Resources for Developers, by Developers
"#,
);
let layout = layout_dom(&tree, (1280.0, 720.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let grid = rect("grid");
let copy = rect("copy");
let art = rect("art");
assert!(
((copy.x + copy.width + 16.0) - art.x).abs() < 0.01,
"the authored column gap must separate the final tracks: {copy:?} {art:?}"
);
assert!(
((art.x + art.width) - (grid.x + grid.width)).abs() < 0.01,
"free-space redistribution must not grow a track after it freezes: {grid:?} {copy:?} {art:?}"
);
}
#[test]
fn empty_block_before_participates_in_normal_flow_geometry() {
let tree = parse_html(
r#"
TEXT
"#,
);
let layout = layout_dom(&tree, (800.0, 600.0));
let host = layout.rects[&tree.get_element_by_id("host").unwrap()];
let next = layout.rects[&tree.get_element_by_id("next").unwrap()];
assert!(
(host.height - 70.0).abs() < 0.01,
"40px generated block + 10px margin + 20px line must size host: {host:?}"
);
assert!(
(next.y - 70.0).abs() < 0.01,
"following flow content must start after generated geometry: {next:?}"
);
}
#[test]
fn empty_inline_block_pseudo_is_an_atomic_flex_item() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (800.0, 600.0));
let label = layout.rects[&tree.get_element_by_id("label").unwrap()];
assert!(
(label.x - 17.0).abs() < 0.01,
"8px pseudo content width + 2px borders + 5px margin must precede label: {label:?}"
);
}
#[test]
fn light_dark_uses_the_final_inherited_color_scheme_across_the_cascade() {
let tree = parse_html(
r#"
"#,
);
let layout = layout_dom(&tree, (800.0, 600.0));
let dark = tree.get_element_by_id("dark").unwrap();
let child = tree.get_element_by_id("child").unwrap();
let mixed = tree.get_element_by_id("mixed").unwrap();
let rect = tree.query_selector("#icon rect").unwrap().unwrap();
let dark_style = &layout.styles[&dark];
assert!(dark_style.color_scheme_dark);
assert_eq!(dark_style.background_color, Some([0x18, 0x19, 0x1b, 0xff]));
assert_eq!(dark_style.color, Some([0xee, 0xee, 0xee, 0xff]));
assert_eq!(dark_style.border_color, Some([0x33, 0x33, 0x33, 0xff]));
assert_eq!(
dark_style
.before_pseudo
.as_deref()
.and_then(|pseudo| pseudo.background_color),
Some([0x00, 0x00, 0xff, 0xff]),
"pseudo must use its host's inherited dark scheme"
);
let child_style = &layout.styles[&child];
assert!(child_style.color_scheme_dark);
let (_, stops) = child_style
.background_gradient
.as_ref()
.expect("dark gradient");
assert_eq!(stops[0].0, [0x10, 0x10, 0x10, 0xff]);
assert_eq!(stops[1].0, [0x20, 0x20, 0x20, 0xff]);
let rect_style = &layout.styles[&rect];
assert!(rect_style.color_scheme_dark);
assert_eq!(rect_style.svg_fill.as_deref(), Some("#51565dff"));
assert_eq!(rect_style.svg_stroke.as_deref(), Some("#000000ff"));
let mixed_style = &layout.styles[&mixed];
assert!(
!mixed_style.color_scheme_dark,
"a scheme list admitting light uses the current light preference"
);
assert_eq!(mixed_style.background_color, Some([0xab, 0xcd, 0xef, 0xff]));
}
/// Chromium 150 maps width/height from the selected onto
/// the associated image as presentation hints. The selected 800/400 source
/// therefore reserves a 500x250 box before it loads; below the media
/// breakpoint, the fallback img's 600/600 ratio reserves 500x500.
#[test]
fn selected_picture_source_dimensions_override_fallback_image_ratio() {
let tree = parse_html(
r#"
"#,
);
let image = tree.get_element_by_id("image").unwrap();
let wide = layout_dom(&tree, (1000.0, 600.0));
let wide_rect = wide.rects[&image];
assert!((wide_rect.width - 500.0).abs() < 0.01, "{wide_rect:?}");
assert!((wide_rect.height - 250.0).abs() < 0.01, "{wide_rect:?}");
assert_eq!(wide.styles[&image].aspect_ratio, Some(2.0));
let narrow = layout_dom(&tree, (700.0, 600.0));
let narrow_rect = narrow.rects[&image];
assert!((narrow_rect.width - 500.0).abs() < 0.01, "{narrow_rect:?}");
assert!((narrow_rect.height - 500.0).abs() < 0.01, "{narrow_rect:?}");
assert_eq!(narrow.styles[&image].aspect_ratio, Some(1.0));
}