test(layout): verify native table heights and aligned painting

This commit is contained in:
ldm0
2026-09-18 14:39:49 +08:00
parent 895ffbf025
commit edc677c00a
2 changed files with 75 additions and 19 deletions
@@ -2872,6 +2872,78 @@ async fn screenshot_preserves_table_cell_dimension_hints_and_avatar_columns() {
.expect("table-cell dimension hints should preserve avatar alignment");
}
#[tokio::test(flavor = "current_thread")]
async fn screenshot_table_row_heights_match_chromium() {
run_page_vm_async_test(async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
let mut page = test_page_vm_with_loader_and_document_url(
&loader, Vec::new(), Url::parse("https://example.com/table-row-heights.html")?,
);
page.vm_mut().set_layout_policy(moli_page_types::LayoutPolicy::OnDemand);
let fixture = include_str!("../../../../tests/fixtures/table-row-heights.html");
page.vm_mut().eval(&format!("document.open();document.write({});document.close()", serde_json::to_string(fixture)?))?;
page.vm_mut().prime_document_lifecycle_processing_and_record_stylesheet_network_results();
page.vm_mut().eval("if (!cases.length) buildTableRowHeightCases()")?;
let expected: serde_json::Value = serde_json::from_str(include_str!("../../../../tests/fixtures/table-row-heights.chromium.json"))?;
let mut failures = Vec::new();
for phase in 0..3 {
page.vm_mut().eval(&format!("setTableRowHeightPhase({phase})"))?;
// Both initial layout and a repeated read must agree with the oracle.
for read in 0..2 {
page.vm_mut().screenshot_layout_snapshot(moli_layout::PaintViewport::new(800, 600, 1.0))?.expect("table layout root");
let actual: serde_json::Value = serde_json::from_str(&page.vm_mut().eval("JSON.stringify(collectTableRowHeights())")?)?;
let actual = actual.as_array().unwrap();
let expected_cases = expected["phases"][phase].as_array().unwrap();
assert_eq!(actual.len(), expected_cases.len());
for (actual, expected) in actual.iter().zip(expected_cases) {
assert_eq!(actual["name"], expected["name"]);
let name = actual["name"].as_str().unwrap();
let actual_rects = actual["rects"].as_array().unwrap();
let expected_rects = expected["rects"].as_array().unwrap();
assert_eq!(actual_rects.len(), expected_rects.len());
for (index, (actual, expected)) in actual_rects.iter().zip(expected_rects).enumerate() {
if (0..4).any(|axis| (actual[axis].as_f64().unwrap() - expected[axis].as_f64().unwrap()).abs() > 0.05) {
failures.push(format!("phase {phase}, read {read}, {name}, rect {index}: {actual} != {expected}"));
}
}
}
}
}
assert!(failures.is_empty(), "{} table geometry differences:\n{}", failures.len(), failures.join("\n"));
// Check that final percentage-dependent baseline alignment reaches
// paint as well as CSSOM, including a subsequent group-height change.
page.vm_mut().eval(r#"
for (const {name,owner} of cases) owner.style.display = name === 'percent-baseline' ? 'block' : 'none';
const paintedCase = cases.find(c => c.name === 'percent-baseline').owner;
paintedCase.querySelectorAll('td > div')[0].style.background = 'rgb(255,0,0)';
paintedCase.querySelectorAll('td > div')[1].style.background = 'rgb(0,255,0)';
"#)?;
for height in [120u32, 160] {
page.vm_mut().eval(&format!("paintedCase.querySelector('tbody').style.height = '{height}px'"))?;
let snapshot = page.vm_mut().screenshot_layout_snapshot(moli_layout::PaintViewport::new(240, 200, 1.0))?.expect("painted table root");
let image = moli_paint::raster_snapshot(&snapshot)?;
for (x, y, color) in [(5, height / 4, [255, 0, 0, 255]), (105, height / 2 - 10, [0, 255, 0, 255])] {
let offset = ((y * image.width + x) * 4) as usize;
assert_eq!(&image.rgba[offset..offset + 4], color, "group height {height}, pixel ({x}, {y})");
}
}
page.vm_mut().eval(r#"
paintedCase.style.display = 'none';
const borderedCase = cases.find(c => c.name === 'empty-groups-collapsed').owner;
borderedCase.style.display = 'block';
borderedCase.querySelector('thead').style.height = '80px';
borderedCase.querySelector('tfoot').style.height = '80px';
"#)?;
let snapshot = page.vm_mut().screenshot_layout_snapshot(moli_layout::PaintViewport::new(240, 200, 1.0))?.expect("collapsed table root");
let image = moli_paint::raster_snapshot(&snapshot)?;
for (x, y) in [(100, 1), (100, 187), (1, 100)] {
let offset = ((y * image.width + x) * 4) as usize;
assert_eq!(&image.rgba[offset..offset + 4], [0, 0, 255, 255], "collapsed border at ({x}, {y})");
}
Ok::<_, anyhow::Error>(())
}).await.expect("table row heights should match Chromium");
}
#[tokio::test(flavor = "current_thread")]
async fn screenshot_recascades_table_part_dimension_hints() {
run_page_vm_async_test(async move {
+3 -19
View File
@@ -36,7 +36,6 @@
];
const checks = [];
const mutations = [];
const cssHeightReferences = [];
for (const dimension of dimensions) {
for (const test of cases) {
const owner = document.createElement('div');
@@ -64,10 +63,8 @@
}
}
// Table/row heights size real rows; zero preserves the content minimum.
// Moli's table formatter currently ignores CSS row-group heights too, so
// compare those native groups with authored CSS. The block cases above
// independently verify the hint values, including percentages and zero.
// Native table, row and row-group hints constrain actual row geometry.
// Zero/removal preserves the content minimum (Chromium 147 oracle).
for (const tag of ['table','thead','tbody','tfoot','tr']) {
const table = document.createElement('table');
table.style.width = '200px';
@@ -81,15 +78,6 @@
document.body.append(table);
const check = {id:`native-${tag}`, element:row,
expected:[[200,32],[200,64],[200,8],[200,8]]};
if (element === section) {
const reference = table.cloneNode(true);
const referenceGroup = reference.querySelector(tag);
referenceGroup.removeAttribute('height');
referenceGroup.style.height = '32px';
document.body.append(reference);
check.reference = reference.rows[0];
cssHeightReferences.push(referenceGroup);
}
checks.push(check);
mutations.push({element, names:['height']});
}
@@ -127,18 +115,14 @@
else if (phase === 3) element.setAttribute(name, '0');
}
}
for (const element of cssHeightReferences) {
element.style.height = ['32px','64px','auto','0px'][phase];
}
}
function collectTablePartDimensionChecks() {
return checks.map(check => {
const rect = check.element && check.element.getBoundingClientRect();
const reference = check.reference && check.reference.getBoundingClientRect();
return {id:check.id,
actual:check.cells ? check.cells.map(cell => cell.getBoundingClientRect().width)
: [rect.width,rect.height],
expected:reference ? [reference.width,reference.height] : check.expected[dimensionPhase]};
expected:check.expected[dimensionPhase]};
});
}
</script>