>()
);
}
#[test]
fn inset_absolute_uses_nearest_positioned_ancestor() {
let html = r##"
"##;
let tree = parse_html(html);
let layout = layout_dom(&tree, (900.0, 700.0));
let cb = layout.rects[&tree.get_element_by_id("cb").expect("containing block")];
let abs = layout.rects[&tree.get_element_by_id("abs").expect("absolute child")];
let abs_end = layout.rects[&tree.get_element_by_id("abs-end").expect("end-inset child")];
let abs_percent = layout.rects[&tree.get_element_by_id("abs-percent").expect("percent-inset child")];
let fixed = layout.rects[&tree.get_element_by_id("fixed").expect("fixed child")];
// Absolute insets are measured from the positioned ancestor's padding
// edge, not from the intervening static wrapper.
assert!((abs.x - cb.x - 25.0).abs() < 1.0, "wrong absolute x: cb={cb:?} abs={abs:?}");
assert!((abs.y - cb.y - 35.0).abs() < 1.0, "wrong absolute y: cb={cb:?} abs={abs:?}");
assert_eq!(
layout.styles[&tree.get_element_by_id("abs").unwrap()].display,
obscura_render::Display::Block,
"positioned inline should be blockified"
);
assert!((abs_end.x - 350.0).abs() < 1.0, "wrong right-inset x: {abs_end:?}");
assert!((abs_end.y - 200.0).abs() < 1.0, "wrong bottom-inset y: {abs_end:?}");
assert!((abs_percent.x - 260.0).abs() < 1.0, "wrong percent-inset x: {abs_percent:?}");
assert!((abs_percent.y - 160.0).abs() < 1.0, "wrong percent-inset y: {abs_percent:?}");
assert!((fixed.x - 520.0).abs() < 1.0, "fixed box did not use viewport x: {fixed:?}");
assert!((fixed.y - 40.0).abs() < 1.0, "fixed box did not use viewport y: {fixed:?}");
}
#[test]
fn transforms_establish_absolute_and_fixed_containing_blocks() {
let html = r##"
"##;
let tree = parse_html(html);
let layout = layout_dom(&tree, (900.0, 700.0));
let transformer_id = tree.get_element_by_id("transformer").unwrap();
let transformer = layout.rects[&transformer_id];
let abs = layout.rects[&tree.get_element_by_id("abs").unwrap()];
let fixed_transformed = layout.rects[&tree.get_element_by_id("fixed-transformed").unwrap()];
let identity_transform = layout.rects[&tree.get_element_by_id("identity-transform").unwrap()];
let identity_abs = layout.rects[&tree.get_element_by_id("identity-abs").unwrap()];
let fixed_viewport = layout.rects[&tree.get_element_by_id("fixed-viewport").unwrap()];
// Layout rects precede paint transforms. Insets use the transformer's
// padding box; the shared translate is recorded separately for the whole
// DOM subtree.
assert!((abs.x - transformer.x - 25.0).abs() < 1.0, "wrong transformed abs x: transformer={transformer:?} abs={abs:?}");
assert!((abs.y - transformer.y - 30.0).abs() < 1.0, "wrong transformed abs y: transformer={transformer:?} abs={abs:?}");
assert!((fixed_transformed.x - transformer.x - 155.0).abs() < 1.0, "wrong transformed fixed x: {fixed_transformed:?}");
assert!((fixed_transformed.y - transformer.y - 105.0).abs() < 1.0, "wrong transformed fixed y: {fixed_transformed:?}");
assert_eq!(layout.translates[&transformer_id], (30.0, 20.0));
assert!((identity_abs.x - identity_transform.x - 70.0).abs() < 1.0, "unsupported transform did not establish abs x: {identity_abs:?}");
assert!((identity_abs.y - identity_transform.y - 50.0).abs() < 1.0, "unsupported transform did not establish abs y: {identity_abs:?}");
assert!((fixed_viewport.x - 700.0).abs() < 1.0, "positioned ancestor captured fixed x: {fixed_viewport:?}");
assert!((fixed_viewport.y - 40.0).abs() < 1.0, "positioned ancestor captured fixed y: {fixed_viewport:?}");
}
#[test]
fn modern_effects_establish_containing_blocks_independently() {
let tree = parse_html(include_str!("../../../render-repros/modern-containing-block-triggers.html"));
let layout = layout_dom(&tree, (900.0, 700.0));
let cases = [
("filter-cb", "filter-badge"),
("perspective-cb", "perspective-badge"),
("contain-cb", "contain-badge"),
("will-change-cb", "will-change-badge"),
("visibility-cb", "visibility-badge"),
];
for (cb_id, badge_id) in cases {
let cb = layout.rects[&tree.get_element_by_id(cb_id).unwrap()];
let badge = layout.rects[&tree.get_element_by_id(badge_id).unwrap()];
assert!((badge.x - cb.x - 75.0).abs() < 1.0, "{badge_id} wrong x: cb={cb:?} badge={badge:?}");
assert!((badge.y - cb.y - 55.0).abs() < 1.0, "{badge_id} wrong y: cb={cb:?} badge={badge:?}");
}
let filter_cb = layout.rects[&tree.get_element_by_id("filter-cb").unwrap()];
let filter_fixed = layout.rects[&tree.get_element_by_id("filter-fixed").unwrap()];
assert!((filter_fixed.x - filter_cb.x - 5.0).abs() < 1.0);
assert!((filter_fixed.y - filter_cb.y - 5.0).abs() < 1.0);
let contain_cb = layout.rects[&tree.get_element_by_id("contain-cb").unwrap()];
let contain_fixed = layout.rects[&tree.get_element_by_id("contain-fixed").unwrap()];
assert!((contain_fixed.x - contain_cb.x - 5.0).abs() < 1.0);
assert!((contain_fixed.y - contain_cb.y - 5.0).abs() < 1.0);
// Pinned Chromium 145 does not make container-type:inline-size a
// positioning containing block. Keep it as a negative control.
let container_badge = layout.rects[&tree.get_element_by_id("container-badge").unwrap()];
assert!((container_badge.x - 775.0).abs() < 1.0, "container-type control x: {container_badge:?}");
assert!((container_badge.y - 175.0).abs() < 1.0, "container-type control y: {container_badge:?}");
}
#[test]
fn long_text_run_wraps_across_multiple_lines() {
// A long single text node with no inline elements breaking it up must
// wrap within a narrow container instead of overflowing on one line. The
// container's height is the mode-independent proof: several wrapped lines
// make it much taller than one line, whether text is shaped by cosmic-text
// (paint) or split into word boxes (layout-only).
let html = r##"This sentence has plenty of words to wrap across several lines
"##;
let tree = parse_html(html);
let layout = layout_dom(&tree, (1000.0, 1000.0));
// Tightest element enclosing the text: the 100px div itself.
let mut div_rect: Option = None;
for (id, rect) in &layout.rects {
if tree.text_content(*id).contains("several lines") {
if div_rect.as_ref().map(|d| rect.width * rect.height < d.width * d.height).unwrap_or(true) {
div_rect = Some(*rect);
}
}
}
let div_rect = div_rect.expect("text container laid out");
assert!(div_rect.width <= 101.0, "container should hold its 100px width, got {}", div_rect.width);
assert!(
div_rect.height > 60.0,
"text should wrap onto several lines in a 100px-wide box (tall container), got height {}",
div_rect.height
);
}
#[test]
fn negative_flex_margin_overlays_at_container_start() {
let html = r##"
"##;
let tree = parse_html(html);
let layout = layout_dom(&tree, (900.0, 1000.0));
let document = layout.rects[&tree.get_element_by_id("document").unwrap()];
let main = layout.rects[&tree.get_element_by_id("main").unwrap()];
let body = layout.rects[&tree.get_element_by_id("body").unwrap()];
let sidebar = layout.rects[&tree.get_element_by_id("sidebar").unwrap()];
assert!((document.x - 0.0).abs() < 0.01, "document: {document:?}");
assert!((main.x - 0.0).abs() < 0.01, "main: {main:?}");
assert!((body.x - 225.0).abs() < 0.01, "body: {body:?}");
assert!((sidebar.x - 0.0).abs() < 0.01, "sidebar: {sidebar:?}");
}
#[test]
fn percentage_flex_overlay_uses_auto_width_parent_content_box() {
let html = r##"
"##;
let tree = parse_html(html);
let layout = layout_dom(&tree, (1280.0, 1400.0));
let document = layout.rects[&tree.get_element_by_id("document").unwrap()];
let main = layout.rects[&tree.get_element_by_id("main").unwrap()];
let body = layout.rects[&tree.get_element_by_id("body").unwrap()];
let sidebar = layout.rects[&tree.get_element_by_id("sidebar").unwrap()];
assert!((document.x - 16.0).abs() < 0.01, "document: {document:?}");
assert!((document.width - 1248.0).abs() < 0.01, "document: {document:?}");
assert!((main.x - 16.0).abs() < 0.01, "main: {main:?}");
assert!((body.x - 336.0).abs() < 0.01, "body: {body:?}");
assert!((sidebar.x - 16.0).abs() < 0.01, "sidebar: {sidebar:?}");
assert!((sidebar.width - 320.0).abs() < 0.01, "sidebar: {sidebar:?}");
}
#[test]
fn float_flow_zone_preserves_blocks_inline_runs_and_clearance() {
let tree = parse_html(include_str!("../../../render-repros/float-flow-bands.html"));
let layout = layout_dom(&tree, (900.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let float = rect("float");
let intro = rect("intro");
let release = rect("release");
let download = rect("download");
let archive = rect("archive");
let sponsors = rect("sponsors");
let logos = rect("logos");
let cleared = rect("cleared");
assert!((float.x - 650.0).abs() < 0.01 && (float.height - 400.0).abs() < 0.01, "float: {float:?}");
assert!((intro.x - 0.0).abs() < 0.01 && (intro.width - 650.0).abs() < 0.01, "intro: {intro:?}");
assert!((release.y - 100.0).abs() < 0.01 && (release.width - 650.0).abs() < 0.01, "release: {release:?}");
assert!((download.x - 0.0).abs() < 0.01 && (download.y - 150.0).abs() < 0.01, "download: {download:?}");
assert!((archive.x - 60.0).abs() < 0.01 && (archive.y - 150.0).abs() < 0.01, "archive: {archive:?}");
assert!((sponsors.y - 180.0).abs() < 0.01, "sponsors: {sponsors:?}");
assert!((logos.y - 230.0).abs() < 0.01, "logos: {logos:?}");
assert!((cleared.y - 400.0).abs() < 0.01 && (cleared.width - 900.0).abs() < 0.01, "cleared: {cleared:?}");
assert!(
!layout.rects.contains_key(&tree.get_element_by_id("mobile-duplicate").unwrap()),
"display:none duplicate generated a box"
);
}
#[test]
fn replaced_image_contributes_intrinsic_size_in_ordered_grid() {
let tree = parse_html(include_str!("../../../render-repros/replaced-grid-order.html"));
let image_id = tree.get_element_by_id("image").unwrap();
let intrinsic = HashMap::from([(image_id, (1.0, 1.0))]);
let layout = layout_dom_with_images(&tree, (900.0, 1000.0), &intrinsic);
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let description = rect("description");
let media = rect("media");
let image = rect("image");
assert!(
(description.x - 0.0).abs() < 0.01
&& (description.y - 0.0).abs() < 0.01
&& (description.width - 300.0).abs() < 0.01,
"description: {description:?}"
);
assert!(
(media.x - 300.0).abs() < 0.01
&& (media.y - 0.0).abs() < 0.01
&& (media.height - 300.0).abs() < 0.01,
"media: {media:?}"
);
assert!(
(image.x - 320.0).abs() < 0.01
&& (image.y - 20.0).abs() < 0.01
&& (image.width - 260.0).abs() < 0.01
&& (image.height - 260.0).abs() < 0.01,
"image: {image:?}"
);
}
#[test]
fn auto_block_wrapper_contains_percentage_replaced_child() {
let tree = parse_html(include_str!("../../../render-repros/replaced-block-wrapper.html"));
let image_id = tree.get_element_by_id("image").unwrap();
let intrinsic = HashMap::from([(image_id, (720.0, 424.0))]);
let layout = layout_dom_with_images(&tree, (900.0, 1000.0), &intrinsic);
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let media = rect("media");
let frame = rect("frame");
let wrapper = rect("wrapper");
let image = rect("image");
assert!(
(media.x - 450.0).abs() < 0.01
&& (media.width - 450.0).abs() < 0.01,
"media: {media:?}"
);
assert!(
(frame.x - 450.0).abs() < 0.01
&& (frame.width - 450.0).abs() < 0.01,
"frame: {frame:?}"
);
assert!(
(wrapper.x - 498.0).abs() < 0.01
&& (wrapper.width - 354.0).abs() < 0.01,
"wrapper: {wrapper:?}"
);
assert!(
(image.x - 498.0).abs() < 0.01
&& (image.width - 354.0).abs() < 0.01
&& (image.height - 208.0).abs() < 0.02,
"image: {image:?}"
);
}
#[test]
fn text_alignment_does_not_shrink_flex_items() {
let tree = parse_html(include_str!("../../../render-repros/text-align-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 stack = rect("stack");
let media = rect("media");
let label = rect("label");
let chip = rect("chip");
assert!(
(stack.width - 400.0).abs() < 0.01
&& (media.x - 20.0).abs() < 0.01
&& (media.width - 360.0).abs() < 0.01,
"stack: {stack:?}, media: {media:?}"
);
assert!(
(label.x - 20.0).abs() < 0.01
&& (label.width - 360.0).abs() < 0.01
&& (chip.x - 150.0).abs() < 0.01
&& (chip.width - 100.0).abs() < 0.01,
"label: {label:?}, chip: {chip:?}"
);
}
#[test]
fn opposing_floats_share_header_band_through_inline_wrapper() {
let tree = parse_html(include_str!("../../../render-repros/opposing-header-floats.html"));
let layout = layout_dom(&tree, (900.0, 1000.0));
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
let logo = rect("logo");
let tagline = rect("tagline");
let menu = rect("menu");
assert!(
(logo.x - 0.0).abs() < 0.01
&& (logo.y - 0.0).abs() < 0.01
&& (logo.width - 200.0).abs() < 0.01
&& (logo.height - 100.0).abs() < 0.01,
"logo: {logo:?}"
);
assert!(
(tagline.x - 600.0).abs() < 0.01
&& (tagline.y - 60.0).abs() < 0.01
&& (tagline.width - 300.0).abs() < 0.01
&& (tagline.height - 40.0).abs() < 0.01,
"tagline: {tagline:?}"
);
assert!(
(menu.x - 0.0).abs() < 0.01
&& (menu.y - 100.0).abs() < 0.01
&& (menu.width - 900.0).abs() < 0.01,
"menu: {menu:?}"
);
}