From 1f85dfce6b6eb14084e7b96cd141528d117d5b52 Mon Sep 17 00:00:00 2001
From: SGavrl <277843151+SGavrl@users.noreply.github.com>
Date: Thu, 23 Jul 2026 13:34:19 +0000
Subject: [PATCH] test: gate flex item geometry
---
crates/obscura-render/src/lib.rs | 31 ++++++++++
crates/obscura-render/tests/layout_test.rs | 67 ++++++++++++++++++++++
render-repros/checks.json | 6 ++
render-repros/flex-whitespace-items.html | 48 ++++++++++++++++
4 files changed, 152 insertions(+)
create mode 100644 render-repros/flex-whitespace-items.html
diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs
index 25bd9a4..abb8a5e 100644
--- a/crates/obscura-render/src/lib.rs
+++ b/crates/obscura-render/src/lib.rs
@@ -874,6 +874,37 @@ mod tests {
assert!((out.children[1].border_box.x - 650.0).abs() < 0.01);
}
+ #[test]
+ fn negative_flex_margin_overlays_without_shifting_items() {
+ let main = make_box(Display::Block, 900.0, 200.0);
+ let sidebar = LayoutStyle {
+ display: Display::Flex,
+ width: Dimension::Px(225.0),
+ height: Dimension::Px(180.0),
+ margin: Edges {
+ left: -900.0,
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+ let root = LayoutNode {
+ style: make_box(Display::Flex, 900.0, 220.0),
+ text: None,
+ children: vec![LayoutNode::leaf(main), LayoutNode::leaf(sidebar)],
+ };
+ let out = layout(&root, (900.0, 220.0));
+ assert!(
+ out.children[0].border_box.x.abs() < 0.01,
+ "main shifted to {:?}",
+ out.children[0].border_box
+ );
+ assert!(
+ out.children[1].border_box.x.abs() < 0.01,
+ "overlay shifted to {:?}",
+ out.children[1].border_box
+ );
+ }
+
#[test]
fn flex_row_lays_out_horizontally() {
let root = LayoutNode {
diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs
index 800be28..ab34617 100644
--- a/crates/obscura-render/tests/layout_test.rs
+++ b/crates/obscura-render/tests/layout_test.rs
@@ -265,3 +265,70 @@ fn long_text_run_wraps_across_multiple_lines() {
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:?}");
+}
diff --git a/render-repros/checks.json b/render-repros/checks.json
index e3e1b1d..ae11772 100644
--- a/render-repros/checks.json
+++ b/render-repros/checks.json
@@ -42,6 +42,12 @@
{"name": "calc combines viewport and root font units", "color": "#7048e8", "x": 122, "y": 180, "width": 100, "height": 40},
{"name": "max padding uses actual viewport", "color": "#c2255c", "x": 60, "y": 240, "width": 20, "height": 40}
],
+ "flex-whitespace-items": [
+ {"name": "direct flex sidebar starts at container edge", "color": "#1971c2", "x": 0, "y": 0, "width": 225, "height": 180},
+ {"name": "direct flex body keeps its intended inset", "color": "#2b8a3e", "x": 225, "y": 0, "width": 675, "height": 200},
+ {"name": "display contents flex sidebar starts at container edge", "color": "#7048e8", "x": 0, "y": 260, "width": 150, "height": 140},
+ {"name": "display contents flex body keeps its intended inset", "color": "#c2255c", "x": 150, "y": 260, "width": 450, "height": 160}
+ ],
"grid-auto-repeat": [
{"name": "auto-fill creates all fitting tracks", "color": "#087f5b", "width": 210, "height": 60, "count": 4},
{"name": "auto-fit collapses empty tracks", "color": "#1971c2", "width": 287, "height": 60, "count": 3}
diff --git a/render-repros/flex-whitespace-items.html b/render-repros/flex-whitespace-items.html
new file mode 100644
index 0000000..f1aca58
--- /dev/null
+++ b/render-repros/flex-whitespace-items.html
@@ -0,0 +1,48 @@
+
+
+
+
+ flex whitespace items
+
+
+
+
+
+
+