Root element now creates a stacking context unconditionally. Refactored display list builder to correctly bucket stacking participants into three groups (negative/zero/positive z-index) per Appendix E steps 2/6/7. Fixed z-index:0 elements painting at step 7 instead of step 6, and descendant stacking contexts being trapped inside positioned z-index:auto ancestors instead of participating in the parent stacking context. Added StackingBuckets struct, unified step-6 rendering with tree-order merge by node_id, and 7 unit tests for creates_stacking_context(). Added 6 golden tests (213-218) and 4 integration tests asserting paint-order invariants programmatically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
246 lines
10 KiB
Rust
246 lines
10 KiB
Rust
//! Integration tests for CSS 2.1 Appendix E stacking context paint order.
|
|
//!
|
|
//! These tests assert specific paint-order invariants that golden tests
|
|
//! cannot enforce — golden files can be silently regenerated with UPDATE_GOLDENS,
|
|
//! masking regressions. These tests verify that display list items appear in
|
|
//! the correct order by inspecting SolidRect colors.
|
|
|
|
use display_list::{build_display_list, DisplayItem};
|
|
use html::HtmlParser;
|
|
use layout::LayoutEngine;
|
|
use shared::Color;
|
|
use style::StyleContext;
|
|
|
|
/// Run the full pipeline and return the display list items.
|
|
fn build_display_items(html: &str) -> Vec<DisplayItem> {
|
|
let html_parser = HtmlParser::new();
|
|
let style_context = StyleContext::new();
|
|
let layout_engine = LayoutEngine::new_proportional();
|
|
|
|
let document = html_parser.parse(html);
|
|
let stylesheets = style_context.extract_stylesheets(&document);
|
|
let inline_styles = style_context.extract_inline_styles(&document);
|
|
let computed_styles = style_context.compute_styles(
|
|
&document,
|
|
&stylesheets,
|
|
&inline_styles,
|
|
Some(&css::Viewport::new(800.0, 600.0)),
|
|
);
|
|
|
|
let layout_tree = layout_engine.layout(&document, &computed_styles, 800.0, 600.0);
|
|
let display_list = build_display_list(&layout_tree);
|
|
display_list.items().to_vec()
|
|
}
|
|
|
|
/// Extract positions of SolidRect items by color from the display list.
|
|
/// Returns indices of matching items in the display list.
|
|
fn solid_rect_indices(items: &[DisplayItem], color: Color) -> Vec<usize> {
|
|
items
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, item)| match item {
|
|
DisplayItem::SolidRect { color: c, .. } if *c == color => Some(i),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Assert that the first SolidRect of color `before` appears before the first
|
|
/// SolidRect of color `after` in the display list.
|
|
fn assert_paints_before(items: &[DisplayItem], before: Color, after: Color, msg: &str) {
|
|
let before_indices = solid_rect_indices(items, before);
|
|
let after_indices = solid_rect_indices(items, after);
|
|
assert!(
|
|
!before_indices.is_empty(),
|
|
"{msg}: expected color {before:?} not found in display list"
|
|
);
|
|
assert!(
|
|
!after_indices.is_empty(),
|
|
"{msg}: expected color {after:?} not found in display list"
|
|
);
|
|
assert!(
|
|
before_indices[0] < after_indices[0],
|
|
"{msg}: expected {before:?} (index {}) to paint before {after:?} (index {})",
|
|
before_indices[0],
|
|
after_indices[0]
|
|
);
|
|
}
|
|
|
|
// Color constants matching the test fixtures
|
|
const RED: Color = Color::new(255, 0, 0, 255);
|
|
const GREEN: Color = Color::new(0, 128, 0, 255);
|
|
const BLUE: Color = Color::new(0, 0, 255, 255);
|
|
const YELLOW: Color = Color::new(255, 255, 0, 255);
|
|
const ORANGE: Color = Color::new(255, 165, 0, 255);
|
|
const LIGHTGRAY: Color = Color::new(211, 211, 211, 255);
|
|
|
|
#[test]
|
|
fn stacking_order_auto_ancestor_does_not_trap_descendant_sc() {
|
|
// CSS 2.1 Appendix E: z-index:auto does NOT create a stacking context.
|
|
// A descendant stacking context (z:5 green) inside a positioned-auto
|
|
// ancestor (yellow) must participate in the parent stacking context
|
|
// (.container z:0), not be trapped inside the auto ancestor.
|
|
//
|
|
// Sibling (z:3 blue) must paint BEFORE child (z:5 green), because
|
|
// both participate in .container's stacking context.
|
|
let items = build_display_items(
|
|
r#"<!DOCTYPE html><html><head><style>
|
|
.container { position: relative; width: 300px; height: 200px;
|
|
background-color: lightgray; z-index: 0; }
|
|
.auto-ancestor { position: relative; width: 200px; height: 150px;
|
|
background-color: yellow; }
|
|
.child-sc { position: absolute; top: 10px; left: 10px;
|
|
width: 100px; height: 80px; background-color: green; z-index: 5; }
|
|
.sibling-sc { position: absolute; top: 50px; left: 100px;
|
|
width: 100px; height: 80px; background-color: blue; z-index: 3; }
|
|
</style></head><body>
|
|
<div class="container">
|
|
<div class="auto-ancestor"><div class="child-sc"></div></div>
|
|
<div class="sibling-sc"></div>
|
|
</div></body></html>"#,
|
|
);
|
|
|
|
// Container bg paints first (step 1)
|
|
assert_paints_before(&items, LIGHTGRAY, YELLOW, "container before auto-ancestor");
|
|
// Auto-ancestor (step 6, positioned-auto) paints before either SC child
|
|
assert_paints_before(&items, YELLOW, BLUE, "auto-ancestor before sibling z:3");
|
|
// Sibling z:3 paints before child z:5 (both at step 7, sorted by z-index)
|
|
assert_paints_before(&items, BLUE, GREEN, "sibling z:3 before child z:5");
|
|
// Child z:5 should appear exactly once (no double-rendering)
|
|
assert_eq!(
|
|
solid_rect_indices(&items, GREEN).len(),
|
|
1,
|
|
"child z:5 should paint exactly once, not be double-rendered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stacking_order_three_buckets_negative_zero_positive() {
|
|
// CSS 2.1 Appendix E: paint order within a stacking context:
|
|
// Step 2: negative z-index children
|
|
// Step 6: z-index:0 stacking contexts + positioned-auto
|
|
// Step 7: positive z-index children
|
|
//
|
|
// HTML deliberately puts them in REVERSE order (pos, zero, neg)
|
|
// to ensure the engine sorts by z-index, not tree order.
|
|
let items = build_display_items(
|
|
r#"<!DOCTYPE html><html><head><style>
|
|
.container { position: relative; width: 300px; height: 200px;
|
|
background-color: lightgray; z-index: 0; }
|
|
.neg { position: absolute; top: 10px; left: 10px;
|
|
width: 100px; height: 60px; background-color: red; z-index: -1; }
|
|
.zero { position: absolute; top: 40px; left: 40px;
|
|
width: 100px; height: 60px; background-color: green; z-index: 0; }
|
|
.pos { position: absolute; top: 70px; left: 70px;
|
|
width: 100px; height: 60px; background-color: blue; z-index: 1; }
|
|
</style></head><body>
|
|
<div class="container">
|
|
<div class="pos"></div>
|
|
<div class="zero"></div>
|
|
<div class="neg"></div>
|
|
</div></body></html>"#,
|
|
);
|
|
|
|
// Step 1: container bg
|
|
assert_paints_before(&items, LIGHTGRAY, RED, "container bg before neg z:-1");
|
|
// Step 2: negative z-index
|
|
assert_paints_before(
|
|
&items,
|
|
RED,
|
|
GREEN,
|
|
"neg z:-1 (step 2) before zero z:0 (step 6)",
|
|
);
|
|
// Step 6: z-index:0
|
|
assert_paints_before(
|
|
&items,
|
|
GREEN,
|
|
BLUE,
|
|
"zero z:0 (step 6) before pos z:1 (step 7)",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stacking_order_z_index_zero_at_step6_not_step7() {
|
|
// z-index:0 stacking contexts must paint at step 6 (alongside
|
|
// positioned-auto elements), NOT at step 7 with positive z-index.
|
|
//
|
|
// Tree order: Auto(red), Zero(blue), Auto2(red).
|
|
// Step 6 must render them in tree order: Auto, Zero, Auto2.
|
|
let items = build_display_items(
|
|
r#"<!DOCTYPE html><html><head><style>
|
|
.container { position: relative; width: 200px; height: 150px;
|
|
background-color: lightgray; z-index: 0; }
|
|
.auto { position: relative; width: 80px; height: 40px;
|
|
background-color: red; }
|
|
.zero { position: relative; width: 80px; height: 40px;
|
|
background-color: blue; z-index: 0; }
|
|
</style></head><body>
|
|
<div class="container">
|
|
<div class="auto">Auto</div>
|
|
<div class="zero">Zero</div>
|
|
<div class="auto">Auto2</div>
|
|
</div></body></html>"#,
|
|
);
|
|
|
|
// All three should paint after the container bg
|
|
assert_paints_before(&items, LIGHTGRAY, RED, "container before auto");
|
|
// Auto (first red) should paint before Zero (blue) — tree order at step 6
|
|
assert_paints_before(
|
|
&items,
|
|
RED,
|
|
BLUE,
|
|
"auto (red) before zero (blue) in tree order",
|
|
);
|
|
// Zero (blue) should paint before Auto2 (second red)
|
|
let red_indices = solid_rect_indices(&items, RED);
|
|
let blue_indices = solid_rect_indices(&items, BLUE);
|
|
assert!(
|
|
red_indices.len() >= 2,
|
|
"expected at least 2 red SolidRects (Auto and Auto2)"
|
|
);
|
|
assert!(
|
|
blue_indices[0] > red_indices[0] && blue_indices[0] < red_indices[1],
|
|
"zero (blue, index {}) should be between first red (index {}) and second red (index {}) for tree order",
|
|
blue_indices[0], red_indices[0], red_indices[1]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stacking_order_nested_atomicity() {
|
|
// CSS 2.1: child stacking contexts paint atomically within their parent's
|
|
// z-order position. A child at z:999 inside a parent at z:1 must NOT
|
|
// paint above a sibling at z:2.
|
|
let items = build_display_items(
|
|
r#"<!DOCTYPE html><html><head><style>
|
|
.container { position: relative; width: 300px; height: 200px;
|
|
background-color: lightgray; z-index: 0; }
|
|
.parent-low { position: absolute; top: 10px; left: 10px;
|
|
width: 120px; height: 120px; background-color: red; z-index: 1; }
|
|
.child-high { position: absolute; top: 10px; left: 10px;
|
|
width: 80px; height: 80px; background-color: orange; z-index: 999; }
|
|
.sibling-mid { position: absolute; top: 50px; left: 50px;
|
|
width: 120px; height: 120px; background-color: blue; z-index: 2; }
|
|
</style></head><body>
|
|
<div class="container">
|
|
<div class="parent-low"><div class="child-high"></div></div>
|
|
<div class="sibling-mid"></div>
|
|
</div></body></html>"#,
|
|
);
|
|
|
|
// parent z:1 paints before sibling z:2
|
|
assert_paints_before(&items, RED, BLUE, "parent z:1 before sibling z:2");
|
|
// child z:999 (inside parent z:1) must paint BEFORE sibling z:2 (atomicity)
|
|
assert_paints_before(
|
|
&items,
|
|
ORANGE,
|
|
BLUE,
|
|
"child z:999 inside parent z:1 must paint before sibling z:2 (atomicity)",
|
|
);
|
|
// child should appear exactly once
|
|
assert_eq!(
|
|
solid_rect_indices(&items, ORANGE).len(),
|
|
1,
|
|
"child z:999 should paint exactly once"
|
|
);
|
|
}
|