diff --git a/.isu/issues.json b/.isu/issues.json index 434cb19..3fbd5f4 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 379, + "next_id": 380, "issues": [ { "id": 1, @@ -4634,6 +4634,19 @@ "author": "piefev", "state": "open", "created_at": "2026-06-19T12:18:13Z" + }, + { + "id": 379, + "repo": "we", + "title": "Bing cached Chromium parity still exceeds threshold after carousel alignment", + "body": "After fixing the relative-position percentage offset that pushed the Bing carousel too low, the scenario still cannot remove the xfail from isu issue 280.\\n\\nRepro:\\n```sh\\ncargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/bing.com.we --out-dir crates/e2e/artifacts\\n```\\n\\nCurrent observed state:\\n- DOM and interaction assertions pass.\\n- The carousel now renders in the same vertical band as the routed cached-Chromium artifact.\\n- The committed Chromium goldens are still stale against the Sichuan Tea snapshot; see isu issue 305.\\n- Comparing the current we output to the cached Chromium artifacts remains far above the harness threshold: desktop differs by about 742360/1228500 px (~60.43%) and mobile by about 226237/329160 px (~68.73%) using ImageMagick with a 1.6% fuzz approximation for the e2e tolerance.\\n- Chromium's cached render shows the white IOTD/modules section below the carousel; the we DOM dump has `.modules_wrapper` but it only contains the hidden `#vs_hidden` anchor, so the module content/height is still absent.\\n\\nArtifacts:\\n- crates/e2e/artifacts/real-web/bing.com/desktop.png\\n- crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.chromium.png\\n- crates/e2e/artifacts/real-web/bing.com/desktop.png.diff.png\\n- crates/e2e/artifacts/real-web/bing.com/mobile.png\\n- crates/e2e/artifacts/real-web/bing.com/mobile.scenario-cache.chromium.png\\n- crates/e2e/artifacts/real-web/bing.com/mobile.png.diff.png\\n- crates/e2e/artifacts/real-web/bing.com/desktop_dom.txt\\n\\nExpected: close the remaining rendering/hydration gaps against cached Chromium, then rebaseline the stale committed goldens and remove the xfail from crates/e2e/scenarios/real-web/bing.com.we.", + "labels": [ + "real-web" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-06-19T12:38:59Z" } ] } diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 3fb595c..dcec5cb 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -716,6 +716,43 @@ fn resolve_relative_vertical(top: LengthOrAuto, bottom: LengthOrAuto, cb_height: } } +fn has_percentage_component(value: LengthOrAuto) -> bool { + match value { + LengthOrAuto::Percentage(_) => true, + LengthOrAuto::Calc(_, pct) => pct != 0.0, + LengthOrAuto::ClampCalc { pct, .. } => pct != 0.0, + LengthOrAuto::Length(_) | LengthOrAuto::Auto => false, + } +} + +fn relative_vertical_offset_has_percentage(b: &LayoutBox) -> bool { + let [top, _, bottom, _] = b.css_offsets; + if !matches!(top, LengthOrAuto::Auto) { + has_percentage_component(top) + } else { + has_percentage_component(bottom) + } +} + +fn relative_child_position_cb_height( + definite_parent_height: Option, + parent_height: f32, + child: &LayoutBox, +) -> f32 { + if let Some(height) = definite_parent_height { + return height; + } + + if child.position == Position::Relative + && matches!(child.css_height, LengthOrAuto::Auto) + && relative_vertical_offset_has_percentage(child) + { + return 0.0; + } + + parent_height +} + // --------------------------------------------------------------------------- // Form control intrinsic sizing // --------------------------------------------------------------------------- @@ -2741,7 +2778,7 @@ fn layout_block_children( // --- Handle floated children --- if is_floated(&parent.children[i]) { - let child_position_cb_height = definite_position_cb_height.unwrap_or(viewport_height); + let child_position_cb_height = definite_position_cb_height.unwrap_or(0.0); layout_float_child( &mut parent.children[i], &mut float_ctx, @@ -2807,7 +2844,7 @@ fn layout_block_children( // `compute_layout` adds `child.margin.top` internally, so compensate. let y_for_child = cursor_y + collapsed_top - child_top_margin; - let child_position_cb_height = definite_position_cb_height.unwrap_or(viewport_height); + let child_position_cb_height = definite_position_cb_height.unwrap_or(0.0); compute_layout( &mut parent.children[i], content_x, @@ -2856,8 +2893,12 @@ fn layout_block_children( } } - let final_position_cb_height = definite_position_cb_height.unwrap_or(parent.rect.height); for child in &mut parent.children { + let final_position_cb_height = relative_child_position_cb_height( + definite_position_cb_height, + parent.rect.height, + child, + ); apply_relative_offset(child, content_width, final_position_cb_height); } } @@ -8112,6 +8153,47 @@ p { margin: 0; } assert_eq!(div_box.relative_offset, (0.0, 0.0)); } + #[test] + fn relative_top_percentage_in_auto_height_parent_uses_zero_reference() { + let mut doc = Document::new(); + let (_, _, body) = make_html_body(&mut doc); + let parent = doc.create_element("div"); + let child = doc.create_element("div"); + let inner = doc.create_element("div"); + let sibling = doc.create_element("div"); + doc.append_child(body, parent); + doc.append_child(parent, child); + doc.append_child(child, inner); + doc.append_child(parent, sibling); + doc.set_attribute(body, "style", "margin:0;"); + doc.set_attribute(parent, "style", "position:relative;width:400px;"); + doc.set_attribute(child, "style", "position:relative;top:calc(100% - 47px);"); + doc.set_attribute(inner, "style", "height:100px;"); + doc.set_attribute(sibling, "style", "height:50px;"); + + let tree = layout_doc(&doc); + let parent_box = find_box(&tree.root, parent).unwrap(); + let child_box = find_box(&tree.root, child).unwrap(); + let sibling_box = find_box(&tree.root, sibling).unwrap(); + + assert!( + (parent_box.rect.height - 150.0).abs() < 0.01, + "parent should keep its normal-flow auto height, got {}", + parent_box.rect.height + ); + assert_eq!(child_box.relative_offset, (0.0, -47.0)); + assert!( + (child_box.rect.y - (parent_box.rect.y - 47.0)).abs() < 0.01, + "child y should use zero as the percentage reference, got {}", + child_box.rect.y + ); + assert!( + (sibling_box.rect.y - (parent_box.rect.y + 100.0)).abs() < 0.01, + "relative offset must not affect sibling normal flow, got {}", + sibling_box.rect.y + ); + } + #[test] fn relative_position_bottom_right() { // bottom: 15px should shift up by 15px (negative direction). @@ -10279,6 +10361,59 @@ body { margin: 0; } ); } + #[test] + fn relative_top_percentage_inside_static_wrapper_abspos_uses_stretched_height() { + let mut doc = Document::new(); + let (_, _, body) = make_html_body(&mut doc); + let wrapper = doc.create_element("div"); + let abs_child = doc.create_element("div"); + let rel_child = doc.create_element("div"); + let tile = doc.create_element("div"); + doc.append_child(body, wrapper); + doc.append_child(wrapper, abs_child); + doc.append_child(abs_child, rel_child); + doc.append_child(rel_child, tile); + doc.set_attribute(body, "style", "margin:0;"); + doc.set_attribute(wrapper, "style", "height:100%;"); + doc.set_attribute( + abs_child, + "style", + "position:absolute;top:calc(5vh + 9.0625rem);bottom:0;left:0;right:0;", + ); + doc.set_attribute( + rel_child, + "style", + "position:relative;top:calc(100% - 12.6875rem);height:100px;", + ); + doc.set_attribute(tile, "style", "height:50px;"); + + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (1365.0, 900.0)).unwrap(); + let tree = layout(&styled, &doc, 1365.0, 900.0, &font, &HashMap::new()); + let abs_box = find_box(&tree.root, abs_child).unwrap(); + let rel_box = find_box(&tree.root, rel_child).unwrap(); + + let expected_abs_y = 45.0 + 145.0; + let expected_abs_height = 900.0 - expected_abs_y; + let expected_rel_y = expected_abs_y + expected_abs_height - 203.0; + assert!( + (abs_box.rect.y - expected_abs_y).abs() < 0.01, + "absolute y should be {expected_abs_y}, got {}", + abs_box.rect.y + ); + assert!( + (abs_box.rect.height - expected_abs_height).abs() < 0.01, + "absolute child should stretch to {expected_abs_height}, got {}", + abs_box.rect.height + ); + assert!( + (rel_box.rect.y - expected_rel_y).abs() < 0.01, + "relative child y should be {expected_rel_y}, got {}", + rel_box.rect.y + ); + } + #[test] fn nested_flex_row_items_do_not_overlap() { // Regression for isu issue 327: a flex row that is itself a flex item